@evoke-platform/ui-components 1.17.0 → 1.18.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 (50) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +8 -4
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +238 -141
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +189 -67
  4. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
  5. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +12 -25
  6. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +4 -5
  7. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +34 -22
  8. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +2 -11
  9. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +6 -34
  10. package/dist/published/components/custom/CriteriaBuilder/utils.js +18 -89
  11. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
  12. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
  13. package/dist/published/components/custom/Form/utils.d.ts +1 -0
  14. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +3 -1
  15. package/dist/published/components/custom/FormV2/FormRenderer.js +9 -4
  16. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +5 -0
  17. package/dist/published/components/custom/FormV2/FormRendererContainer.js +116 -53
  18. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -0
  19. package/dist/published/components/custom/FormV2/components/Body.js +4 -2
  20. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -0
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +46 -17
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +44 -11
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +41 -29
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
  30. package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +1 -0
  31. package/dist/published/components/custom/FormV2/components/FormletRenderer.js +6 -14
  32. package/dist/published/components/custom/FormV2/components/HtmlView.js +4 -0
  33. package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.d.ts +2 -0
  34. package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.js +15 -0
  35. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +18 -19
  36. package/dist/published/components/custom/FormV2/components/types.d.ts +8 -2
  37. package/dist/published/components/custom/FormV2/components/utils.d.ts +11 -8
  38. package/dist/published/components/custom/FormV2/components/utils.js +194 -78
  39. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +180 -2
  40. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +212 -0
  41. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +26 -10
  42. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +1 -0
  43. package/dist/published/components/custom/index.d.ts +1 -0
  44. package/dist/published/index.d.ts +1 -1
  45. package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
  46. package/dist/published/stories/FormRenderer.stories.d.ts +9 -3
  47. package/dist/published/stories/FormRenderer.stories.js +1 -0
  48. package/dist/published/stories/FormRendererContainer.stories.d.ts +25 -0
  49. package/dist/published/theme/hooks.d.ts +1 -0
  50. package/package.json +3 -2
@@ -1,13 +1,13 @@
1
1
  import React from 'react';
2
2
  import { useApiServices } from '@evoke-platform/context';
3
- import { getPrefixedUrl } from './utils';
3
+ import { convertToReadOnly, getPrefixedUrl } from './utils';
4
4
  import { useQuery } from '@tanstack/react-query';
5
5
  import { RecursiveEntryRenderer } from './RecursiveEntryRenderer';
6
- import { Skeleton, Typography } from '../../../core';
7
- import { Box } from '../../../layout';
8
- import { WarningRounded } from '../../../../icons';
6
+ import { Skeleton } from '../../../core';
7
+ import ViewDetailsEntryRenderer from '../../ViewDetailsV2/InstanceEntryRenderer';
8
+ import MisconfiguredErrorMessage from './MisconfiguredErrorMessage';
9
9
  function FormletRenderer(props) {
10
- const { entry } = props;
10
+ const { entry, readOnly } = props;
11
11
  const apiServices = useApiServices();
12
12
  const { data: formlet, isLoading } = useQuery({
13
13
  queryKey: [entry.formletId, 'formlet'],
@@ -17,14 +17,6 @@ function FormletRenderer(props) {
17
17
  });
18
18
  if (isLoading)
19
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")));
20
+ return formlet ? (React.createElement(React.Fragment, null, formlet.entries?.map((formletEntry, index) => readOnly === true ? (React.createElement(ViewDetailsEntryRenderer, { key: index, entry: convertToReadOnly(formletEntry) })) : (React.createElement(RecursiveEntryRenderer, { key: index, entry: formletEntry }))))) : (React.createElement(MisconfiguredErrorMessage, null));
29
21
  }
30
22
  export default FormletRenderer;
@@ -43,6 +43,10 @@ const HtmlView = ({ value }) => {
43
43
  border: 'none',
44
44
  minHeight: 20,
45
45
  },
46
+ // Override Quill list markers for correct display
47
+ '.ql-editor ol li:before': {
48
+ content: '""',
49
+ },
46
50
  } },
47
51
  React.createElement("div", { ref: containerRef }))));
48
52
  };
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function MisconfiguredErrorMessage(): React.JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { WarningRounded } from '../../../../icons';
2
+ import { Typography } from '../../../core';
3
+ import { Box } from '../../../layout';
4
+ import React from 'react';
5
+ export default function MisconfiguredErrorMessage() {
6
+ return (React.createElement(Box, { sx: {
7
+ display: 'flex',
8
+ backgroundColor: '#ffc1073b',
9
+ borderRadius: '8px',
10
+ padding: '16.5px 14px',
11
+ marginTop: '6px',
12
+ } },
13
+ React.createElement(WarningRounded, { sx: { paddingRight: '8px' }, color: "warning" }),
14
+ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "This field was not configured correctly")));
15
+ }
@@ -1,11 +1,9 @@
1
1
  import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
2
- import { WarningRounded } from '@mui/icons-material';
3
2
  import { Grid } from '@mui/material';
4
3
  import { isEmpty } from 'lodash';
5
4
  import React, { useMemo } from 'react';
6
5
  import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
7
- import { Skeleton, TextField, Typography } from '../../../core';
8
- import { Box } from '../../../layout';
6
+ import { Skeleton, TextField } from '../../../core';
9
7
  import FormField from '../../FormField';
10
8
  import AccordionSections from './AccordionSections';
11
9
  import FieldWrapper from './FieldWrapper';
@@ -14,6 +12,7 @@ import DropdownRepeatableField from './FormFieldTypes/CollectionFiles/DropdownRe
14
12
  import RepeatableField from './FormFieldTypes/CollectionFiles/RepeatableField';
15
13
  import Criteria from './FormFieldTypes/Criteria';
16
14
  import { Document } from './FormFieldTypes/DocumentFiles/Document';
15
+ import { FileContent } from './FormFieldTypes/FileContent';
17
16
  import { Image } from './FormFieldTypes/Image';
18
17
  import ObjectPropertyInput from './FormFieldTypes/relatedObjectFiles/ObjectPropertyInput';
19
18
  import UserProperty from './FormFieldTypes/UserProperty';
@@ -22,6 +21,7 @@ import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, f
22
21
  import HtmlView from './HtmlView';
23
22
  import { useQuery } from '@tanstack/react-query';
24
23
  import FormletRenderer from './FormletRenderer';
24
+ import MisconfiguredErrorMessage from './MisconfiguredErrorMessage';
25
25
  function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors, validation) {
26
26
  return {
27
27
  inputId: entryId,
@@ -87,10 +87,9 @@ export function RecursiveEntryRenderer(props) {
87
87
  },
88
88
  });
89
89
  const memorizedCriteria = useMemo(() => {
90
- return 'criteria' in validation && validation.criteria && getValues
91
- ? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
92
- : undefined;
93
- }, [validation, getValues && getValues(), userAccount, instance]);
90
+ const criteria = 'criteria' in validation && validation.criteria ? validation.criteria : display?.criteria;
91
+ return getValues && criteria ? updateCriteriaInputs(criteria, getValues(), userAccount, instance) : undefined;
92
+ }, [validation, getValues && getValues(), userAccount, instance, display]);
94
93
  const memorizedDefaultValueCriteria = useMemo(() => {
95
94
  return display?.defaultValue &&
96
95
  typeof display.defaultValue === 'object' &&
@@ -154,11 +153,15 @@ export function RecursiveEntryRenderer(props) {
154
153
  }
155
154
  }
156
155
  else if (fieldDefinition.type === 'richText') {
157
- return (React.createElement(FieldWrapper, { key: `${entryId}-${fieldValue}`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, RichTextEditor ? (React.createElement(RichTextEditor
156
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, RichTextEditor ? (React.createElement(RichTextEditor
158
157
  // RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
159
158
  , {
160
159
  // RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
161
- id: entry.uniqueId, value: fieldValue, handleUpdate: (value) => handleChange?.(entryId, value), format: "rtf", disabled: entry.type === 'readonlyField', rows: display?.rowCount, hasError: !!errors?.[entryId] })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
160
+ id: entry.uniqueId, value: fieldValue, handleUpdate: (value) => handleChange?.(entryId, value), onBlur: () => {
161
+ onAutosave?.(entryId)?.catch((error) => {
162
+ console.error('Autosave failed:', error);
163
+ });
164
+ }, format: "rtf", disabled: entry.type === 'readonlyField', rows: display?.rowCount, hasError: !!errors?.[entryId] })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
162
165
  onAutosave?.(entryId)?.catch((error) => {
163
166
  console.error('Autosave failed:', error);
164
167
  });
@@ -166,7 +169,11 @@ export function RecursiveEntryRenderer(props) {
166
169
  }
167
170
  else if (fieldDefinition.type === 'document' || fieldDefinition.type === 'file') {
168
171
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
169
- React.createElement(Document, { id: entryId, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
172
+ React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
173
+ }
174
+ else if (fieldDefinition.type === 'fileContent') {
175
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
176
+ React.createElement(FileContent, { id: entryId, error: !!errors?.[entryId], value: fieldValue, hasDescription: !!display?.description, canUpdateProperty: !(entry.type === 'readonlyField'), validate: validation })));
170
177
  }
171
178
  else if (fieldDefinition.type === 'criteria') {
172
179
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -230,15 +237,7 @@ export function RecursiveEntryRenderer(props) {
230
237
  return filteredEntry ? (isSmallerThanMd ? (React.createElement(AccordionSections, { entry: filteredEntry })) : (React.createElement(FormSections, { entry: filteredEntry }))) : null;
231
238
  }
232
239
  else if (!fieldDefinition) {
233
- return (React.createElement(Box, { sx: {
234
- display: 'flex',
235
- backgroundColor: '#ffc1073b',
236
- borderRadius: '8px',
237
- padding: '16.5px 14px',
238
- marginTop: '6px',
239
- } },
240
- React.createElement(WarningRounded, { sx: { paddingRight: '8px' }, color: "warning" }),
241
- React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "This field was not configured correctly")));
240
+ return React.createElement(MisconfiguredErrorMessage, null);
242
241
  }
243
242
  return null;
244
243
  }
@@ -12,9 +12,10 @@ export type FieldAddress = {
12
12
  export type AccessCheck = {
13
13
  result: boolean;
14
14
  };
15
- export type SavedDocumentReference = {
15
+ export type DocumentReference = {
16
16
  id: string;
17
17
  name: string;
18
+ unsaved?: boolean;
18
19
  };
19
20
  export type Document = {
20
21
  id: string;
@@ -38,7 +39,8 @@ export type FileInstance = {
38
39
  export type SimpleEditorProps = {
39
40
  id: string;
40
41
  value: string;
41
- handleUpdate?: (value: string) => void;
42
+ handleUpdate?: (value: string) => void | Promise<void>;
43
+ onBlur?: () => void;
42
44
  format: 'rtf' | 'plain' | 'openxml';
43
45
  disabled?: boolean;
44
46
  rows?: number;
@@ -112,3 +114,7 @@ export type InstanceLink = {
112
114
  id: string;
113
115
  objectId: string;
114
116
  };
117
+ export type FileUploadBatchResult = {
118
+ errorMessage?: string;
119
+ successfulUploads: DocumentReference[];
120
+ };
@@ -1,10 +1,10 @@
1
1
  /// <reference types="react" />
2
- import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount, ObjWithRoot, PanelViewEntry } from '@evoke-platform/context';
2
+ import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, ObjWithRoot, PanelViewEntry, 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 { InstanceLink, SavedDocumentReference } from './types';
7
+ import { DocumentReference, FileUploadBatchResult, InstanceLink } 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;
@@ -54,16 +54,13 @@ export declare const convertPropertiesToParams: (object: Obj) => InputParameter[
54
54
  export declare function getUnnestedEntries(entries: FormEntry[]): FormEntry[];
55
55
  export declare const isEmptyWithDefault: (fieldValue: unknown, entry: InputParameterReference | InputField, instance: Record<string, unknown> | object) => boolean | "" | 0 | undefined;
56
56
  export declare const docProperties: Property[];
57
- /**
58
- * Upload files using the POST /files endpoint for sys__file objects
59
- */
60
- export declare const uploadFiles: (files: (File | SavedDocumentReference)[], apiServices: ApiServices, actionId?: string, metadata?: Record<string, string>, linkTo?: InstanceLink) => Promise<SavedDocumentReference[]>;
57
+ export declare const uploadFiles: (files: (File | DocumentReference)[], apiServices: ApiServices, actionId?: string, fileObjectId?: string, linkTo?: InstanceLink, shortCircuit?: boolean) => Promise<FileUploadBatchResult>;
61
58
  /**
62
59
  * Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
63
60
  * This is used after instance creation when the instance ID becomes available
64
61
  */
65
- export declare const createFileLinks: (files: SavedDocumentReference[], linkedInstance: InstanceLink, apiServices: ApiServices) => Promise<void>;
66
- export declare const uploadDocuments: (files: (File | SavedDocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<SavedDocumentReference[]>;
62
+ export declare const createFileLinks: (fileReferences: DocumentReference[], linkedInstance: InstanceLink, apiServices: ApiServices) => Promise<void>;
63
+ export declare const uploadDocuments: (files: (File | DocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<DocumentReference[]>;
67
64
  export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
68
65
  showAlert: boolean;
69
66
  message?: string;
@@ -89,6 +86,7 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
89
86
  }>>, associatedObject?: {
90
87
  instanceId: string;
91
88
  propertyId: string;
89
+ objectId?: string;
92
90
  }, parameters?: InputParameter[]): Promise<FieldValues>;
93
91
  export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
94
92
  export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object?: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
@@ -107,6 +105,10 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | Pa
107
105
  export declare function plainTextToRtf(plainText: string): string;
108
106
  export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object?: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
109
107
  export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
108
+ export declare function handleFileUpload(apiServices: ApiServices, submission: FieldValues, actionId: string, objectId?: string, instanceId?: string, linkTo?: {
109
+ instanceId: string;
110
+ objectId: string;
111
+ }): Promise<ObjectInstance>;
110
112
  export declare function useFormById(formId: string, apiServices: ApiServices, errorMessage?: string): import("@tanstack/react-query/build/legacy/types").UseQueryResult<EvokeForm, Error>;
111
113
  /**
112
114
  * Extract all values from a criteria/filter object.
@@ -125,3 +127,4 @@ export declare function useFormById(formId: string, apiServices: ApiServices, er
125
127
  export declare const extractPresetValuesFromCriteria: (criteria: Record<string, unknown>) => string[];
126
128
  export declare const extractAllCriteria: (flattenFormEntries: FormEntry[], parameters: InputParameter[]) => Record<string, unknown>[];
127
129
  export declare const extractPresetValuesFromDynamicDefaultValues: (flattenFormEntries: FormEntry[]) => string[];
130
+ export declare function convertToReadOnly(entry: FormEntry): FormEntry;
@@ -1,4 +1,5 @@
1
1
  import { LocalDateTime } from '@js-joda/core';
2
+ import { useQuery } from '@tanstack/react-query';
2
3
  import { jsonLogic } from 'json-logic-js-graphql';
3
4
  import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } from 'lodash';
4
5
  import { DateTime } from 'luxon';
@@ -6,7 +7,6 @@ import { nanoid } from 'nanoid';
6
7
  import Handlebars from 'no-eval-handlebars';
7
8
  import { defaultRuleProcessorMongoDB, formatQuery } from 'react-querybuilder';
8
9
  import { parseMongoDB } from '../../util';
9
- import { useQuery } from '@tanstack/react-query';
10
10
  export const scrollIntoViewWithOffset = (el, offset, container) => {
11
11
  const elementRect = el.getBoundingClientRect();
12
12
  const containerRect = container ? container.getBoundingClientRect() : document.body.getBoundingClientRect();
@@ -117,6 +117,16 @@ export const getEntryId = (entry) => {
117
117
  ? entry.input.id
118
118
  : undefined;
119
119
  };
120
+ const getEntryType = (entry, parameters) => {
121
+ if (entry?.type === 'inputField') {
122
+ return entry?.input?.type;
123
+ }
124
+ else if (entry?.type === 'input') {
125
+ // For 'input' type entries, look up the parameter by parameterId
126
+ const parameter = parameters?.find((param) => param.id === entry.parameterId);
127
+ return parameter?.type;
128
+ }
129
+ };
120
130
  /**
121
131
  * Returns editable field IDs that are currently visible on the form.
122
132
  */
@@ -167,6 +177,7 @@ export function getPrefixedUrl(url) {
167
177
  '/documents',
168
178
  '/payments',
169
179
  '/forms',
180
+ '/files',
170
181
  '/formlets',
171
182
  '/locations',
172
183
  ];
@@ -509,53 +520,85 @@ export const docProperties = [
509
520
  type: 'string',
510
521
  },
511
522
  ];
512
- /**
513
- * Upload files using the POST /files endpoint for sys__file objects
514
- */
515
- export const uploadFiles = async (files, apiServices, actionId = '_create', metadata, linkTo) => {
523
+ export const uploadFiles = async (files, apiServices, actionId = '_create', fileObjectId = 'sys__file', linkTo, shortCircuit = true) => {
516
524
  // Separate already uploaded files from files that need uploading
517
525
  const alreadyUploaded = files.filter((file) => !('size' in file));
518
526
  const filesToUpload = files.filter((file) => 'size' in file);
519
- // Upload all files in parallel
520
- const uploadPromises = filesToUpload.map(async (file) => {
527
+ let failedUpload = false;
528
+ // Upload all files in parallel, handling each result individually
529
+ const uploadPromises = [];
530
+ for (const file of filesToUpload) {
521
531
  const formData = new FormData();
522
532
  formData.append('file', file);
523
533
  formData.append('actionId', actionId);
524
- formData.append('objectId', 'sys__file');
525
- if (metadata) {
526
- for (const [key, value] of Object.entries(metadata)) {
527
- formData.append(key, value);
528
- }
529
- }
534
+ formData.append('objectId', fileObjectId);
535
+ formData.append('input', JSON.stringify({ name: file.name, contentType: file.type }));
530
536
  if (linkTo) {
531
537
  formData.append('linkTo', JSON.stringify(linkTo));
532
538
  }
533
- const fileInstance = await apiServices.post(getPrefixedUrl(`/files`), formData);
539
+ const uploadPromise = (async () => {
540
+ try {
541
+ const fileInstance = await apiServices.post(getPrefixedUrl(`/files`), formData);
542
+ return {
543
+ id: fileInstance.id,
544
+ name: fileInstance.name,
545
+ unsaved: true,
546
+ };
547
+ }
548
+ catch (error) {
549
+ console.error(`Failed to upload file ${file.name}:`, error);
550
+ failedUpload = true;
551
+ }
552
+ })();
553
+ uploadPromises.push(uploadPromise);
554
+ }
555
+ if (!shortCircuit) {
556
+ // Wait for all upload attempts to complete (successes and failures)
557
+ const uploadResults = await Promise.allSettled(uploadPromises);
558
+ const uploadedFiles = uploadResults
559
+ .filter((result) => result.status === 'fulfilled' && !!result.value)
560
+ .map((result) => result.value);
561
+ const failedCount = filesToUpload.length - uploadedFiles.length;
534
562
  return {
535
- id: fileInstance.id,
536
- name: fileInstance.name,
563
+ successfulUploads: [...alreadyUploaded, ...uploadedFiles],
564
+ errorMessage: failedCount > 0 ? `Failed to upload ${failedCount} file(s)` : undefined,
537
565
  };
538
- });
566
+ }
539
567
  const uploadedFiles = await Promise.all(uploadPromises);
540
- return [...alreadyUploaded, ...uploadedFiles];
568
+ if (failedUpload) {
569
+ return {
570
+ successfulUploads: [],
571
+ errorMessage: 'An error occurred when uploading files',
572
+ };
573
+ }
574
+ return {
575
+ successfulUploads: [...alreadyUploaded, ...uploadedFiles],
576
+ };
541
577
  };
542
578
  /**
543
579
  * Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
544
580
  * This is used after instance creation when the instance ID becomes available
545
581
  */
546
- export const createFileLinks = async (files, linkedInstance, apiServices) => {
547
- const linkPromises = files.map(async (file) => {
548
- await apiServices.post(getPrefixedUrl(`/objects/sys__fileLink/instances`), {
549
- name: 'File Link',
550
- file: { id: file.id, name: file.name },
551
- linkedInstance,
552
- }, {
553
- params: {
554
- actionId: '_create',
555
- },
556
- });
557
- });
558
- await Promise.all(linkPromises);
582
+ export const createFileLinks = async (fileReferences, linkedInstance, apiServices) => {
583
+ // Link files in parallel, handling each result individually
584
+ const linkPromises = [];
585
+ for (const file of fileReferences) {
586
+ const linkPromise = (async () => {
587
+ try {
588
+ await apiServices.post(getPrefixedUrl(`/objects/sys__fileLink/instances/actions`), {
589
+ actionId: '_create',
590
+ input: { name: 'File Link', file: { id: file.id, name: file.name }, linkedInstance },
591
+ });
592
+ }
593
+ catch (error) {
594
+ console.error(`Failed to create file link for ${file.name}:`, error);
595
+ // The file remains unlinked and can be retried later
596
+ }
597
+ })();
598
+ linkPromises.push(linkPromise);
599
+ }
600
+ // Wait for all linking attempts to complete (successes and failures)
601
+ await Promise.allSettled(linkPromises);
559
602
  };
560
603
  export const uploadDocuments = async (files, metadata, apiServices, instanceId, objectId) => {
561
604
  const allDocuments = [];
@@ -583,20 +626,29 @@ export const uploadDocuments = async (files, metadata, apiServices, instanceId,
583
626
  export const deleteDocuments = async (submittedFields, requestSuccess, apiServices, object, instance, action, setSnackbarError) => {
584
627
  const documentProperties = action?.parameters
585
628
  ? action.parameters.filter((param) => ['document', 'file'].includes(param.type))
586
- : object?.properties?.filter((prop) => ['document', 'file'].includes(prop.type));
629
+ : object.properties?.filter((prop) => ['document', 'file'].includes(prop.type));
587
630
  for (const docProperty of documentProperties ?? []) {
588
631
  const savedValue = submittedFields[docProperty.id];
589
- const originalValue = instance?.[docProperty.id];
632
+ const originalValue = instance[docProperty.id];
590
633
  const documentsToRemove = requestSuccess
591
634
  ? (originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? [])
592
635
  : (savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? []);
593
636
  for (const doc of documentsToRemove) {
594
637
  try {
595
- // Use different endpoints based on property type
596
- const deleteEndpoint = docProperty.type === 'file'
597
- ? getPrefixedUrl(`/files/${doc.id}`)
598
- : getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/${doc.id}`);
599
- await apiServices?.delete(deleteEndpoint);
638
+ // Build context for state model
639
+ const fieldType = docProperty.type === 'file' ? 'file' : 'document';
640
+ if (fieldType === 'file') {
641
+ // For file properties, unlink the file. Don't delete the actual file
642
+ // since other instances may be using it.
643
+ await apiServices.post(getPrefixedUrl(`/files/${doc.id}/unlinkInstance`), {
644
+ linkedInstanceId: instance.id,
645
+ linkedObjectId: object.id,
646
+ });
647
+ }
648
+ else {
649
+ // For document properties, delete the document
650
+ await apiServices.delete(getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/${doc.id}`));
651
+ }
600
652
  }
601
653
  catch (error) {
602
654
  if (error) {
@@ -611,16 +663,28 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
611
663
  }
612
664
  }
613
665
  };
614
- const getEntryType = (entry, parameters) => {
615
- if (entry?.type === 'inputField') {
616
- return entry.input.type;
666
+ async function handleUploads(files, propertyType, entry, apiServices, objectId, instanceId) {
667
+ if (propertyType === 'file') {
668
+ const fileObjectId = entry.display?.fileObjectId ?? 'sys__file';
669
+ const createActionId = entry.display?.createActionId ?? '_create';
670
+ return await uploadFiles(files, apiServices, createActionId, fileObjectId, instanceId ? { id: instanceId, objectId } : undefined);
617
671
  }
618
- else if (entry?.type === 'input') {
619
- // For 'input' type entries, look up the parameter by parameterId
620
- const parameter = parameters?.find((param) => param.id === entry.parameterId);
621
- return parameter?.type;
672
+ else if (propertyType === 'document' && instanceId) {
673
+ try {
674
+ const docs = await uploadDocuments(files, {
675
+ type: '',
676
+ view_permission: '',
677
+ ...entry?.documentMetadata,
678
+ }, apiServices, instanceId, objectId);
679
+ return { successfulUploads: docs };
680
+ }
681
+ catch (error) {
682
+ console.error('Error uploading documents:', error);
683
+ return { successfulUploads: [], errorMessage: 'Error uploading documents' };
684
+ }
622
685
  }
623
- };
686
+ return { successfulUploads: [] };
687
+ }
624
688
  /**
625
689
  * Transforms a form submission into a format safe for API submission.
626
690
  *
@@ -638,44 +702,36 @@ export async function formatSubmission(submission, apiServices, objectId, instan
638
702
  if (associatedObject) {
639
703
  delete submission[associatedObject.propertyId];
640
704
  }
641
- const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
705
+ const allEntries = getUnnestedEntries(form?.entries ?? []);
642
706
  for (const [key, value] of Object.entries(submission)) {
643
707
  const entry = allEntries?.find((entry) => getEntryId(entry) === key);
644
708
  if (isArray(value)) {
645
- // Only upload if array contains File instances (not SavedDocumentReference)
709
+ const propertyType = getEntryType(entry, parameters);
710
+ // The only array types we need to handle specially are 'file' and 'document'.
711
+ if (propertyType !== 'file' && propertyType !== 'document') {
712
+ continue;
713
+ }
714
+ // Only upload if array contains File instances (not SavedDocumentReference).
646
715
  const fileInArray = value.some((item) => item instanceof File);
647
716
  if (fileInArray && apiServices && objectId) {
648
- // Determine property type from the entry
649
- const parameterType = getEntryType(entry, parameters);
650
- try {
651
- let uploadedDocuments = [];
652
- if (parameterType === 'file') {
653
- uploadedDocuments = await uploadFiles(value, apiServices, '_create', {
654
- ...entry?.documentMetadata,
655
- },
656
- // Only pass linkTo if instanceId exists (update action)
657
- instanceId ? { id: instanceId, objectId } : undefined);
658
- }
659
- else if (parameterType === 'document' && instanceId) {
660
- uploadedDocuments = await uploadDocuments(value, {
661
- type: '',
662
- view_permission: '',
663
- ...entry?.documentMetadata,
664
- }, apiServices, instanceId, objectId);
665
- }
666
- submission[key] = uploadedDocuments;
667
- }
668
- catch (err) {
669
- if (err) {
670
- setSnackbarError &&
671
- setSnackbarError({
672
- showAlert: true,
673
- message: `An error occurred while uploading associated ${parameterType === 'file' ? 'files' : 'documents'}`,
674
- isError: true,
675
- });
676
- }
717
+ const result = await handleUploads(value, propertyType ?? '', entry, apiServices, objectId, instanceId);
718
+ if (result.errorMessage) {
719
+ setSnackbarError?.({
720
+ showAlert: true,
721
+ // Provide generic message since we're ignoring a partial upload.
722
+ message: `An error occurred while uploading associated ${propertyType === 'file' ? 'files' : 'documents'}`,
723
+ isError: true,
724
+ });
677
725
  return submission;
678
726
  }
727
+ submission[key] = result.successfulUploads;
728
+ }
729
+ else {
730
+ submission[key] = value
731
+ // This should never happen but it's possible that the else branch
732
+ // is reached because either 'apiServices' or 'objectId' is undefined.
733
+ // If that's the case the submission will fail if we submit File blobs.
734
+ .filter((file) => !(file instanceof File));
679
735
  }
680
736
  // if there are address fields with no value address needs to be set to undefined to be able to submit
681
737
  }
@@ -689,7 +745,8 @@ export async function formatSubmission(submission, apiServices, objectId, instan
689
745
  submission[key] =
690
746
  entry &&
691
747
  ['input', 'inputField'].includes(entry.type) &&
692
- entry.display?.relatedObjectId
748
+ (entry.display?.relatedObjectId ||
749
+ associatedObject?.objectId)
693
750
  ? pick(value, 'id', 'name', 'objectId')
694
751
  : pick(value, 'id', 'name');
695
752
  }
@@ -921,6 +978,28 @@ function applyMaskToObfuscatedValue(value, mask) {
921
978
  }
922
979
  return maskedValue;
923
980
  }
981
+ export async function handleFileUpload(apiServices, submission, actionId, objectId, instanceId, linkTo) {
982
+ const formData = new FormData();
983
+ if (submission['content'] instanceof File && submission['content'].size !== 0) {
984
+ formData.append('file', submission['content']);
985
+ }
986
+ formData.append('objectId', objectId ?? 'sys__file');
987
+ formData.append('actionId', actionId);
988
+ delete submission['content'];
989
+ formData.append('input', JSON.stringify(submission));
990
+ if (instanceId) {
991
+ return await apiServices.patch(getPrefixedUrl(`/files/${instanceId}`), formData);
992
+ }
993
+ else {
994
+ if (linkTo) {
995
+ formData.append('linkTo', JSON.stringify({
996
+ id: linkTo?.instanceId,
997
+ objectId: linkTo?.objectId,
998
+ }));
999
+ }
1000
+ return await apiServices.post(getPrefixedUrl(`/files`), formData);
1001
+ }
1002
+ }
924
1003
  export function useFormById(formId, apiServices, errorMessage) {
925
1004
  return useQuery({
926
1005
  queryKey: ['form', formId],
@@ -1041,3 +1120,40 @@ export const extractPresetValuesFromDynamicDefaultValues = (flattenFormEntries)
1041
1120
  }
1042
1121
  return allPresetValues;
1043
1122
  };
1123
+ export function convertToReadOnly(entry) {
1124
+ if (entry.type === 'columns') {
1125
+ const columns = entry.columns.map((column) => ({
1126
+ ...column,
1127
+ entries: column.entries?.map((entry) => convertToReadOnly(entry)),
1128
+ }));
1129
+ return {
1130
+ ...entry,
1131
+ columns: columns,
1132
+ };
1133
+ }
1134
+ else if (entry.type === 'sections') {
1135
+ const sections = entry.sections.map((section) => ({
1136
+ ...section,
1137
+ entries: section.entries?.map((entry) => convertToReadOnly(entry)),
1138
+ }));
1139
+ return {
1140
+ ...entry,
1141
+ sections: sections,
1142
+ };
1143
+ }
1144
+ else if (entry.type === 'input') {
1145
+ return {
1146
+ type: 'readonlyField',
1147
+ propertyId: entry.parameterId,
1148
+ display: entry.display,
1149
+ };
1150
+ }
1151
+ else if (entry.type === 'inputField') {
1152
+ return {
1153
+ type: 'readonlyField',
1154
+ propertyId: entry.input.id,
1155
+ display: entry.display,
1156
+ };
1157
+ }
1158
+ return entry;
1159
+ }