@evoke-platform/ui-components 1.13.0-dev.5 → 1.13.0-dev.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +93 -86
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +104 -184
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +51 -32
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +40 -38
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +95 -169
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +12 -6
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +39 -17
- package/dist/published/components/custom/FormV2/components/types.d.ts +6 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +10 -11
- package/dist/published/components/custom/FormV2/components/utils.js +169 -93
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -15
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +38 -46
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +38 -13
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
- package/package.json +3 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
|
|
2
2
|
import { WarningRounded } from '@mui/icons-material';
|
|
3
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
4
|
import DOMPurify from 'dompurify';
|
|
4
5
|
import { isEmpty } from 'lodash';
|
|
5
|
-
import React, {
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
6
7
|
import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
|
|
7
|
-
import { TextField, Typography } from '../../../core';
|
|
8
|
+
import { Skeleton, TextField, Typography } from '../../../core';
|
|
8
9
|
import { Box } from '../../../layout';
|
|
9
10
|
import FormField from '../../FormField';
|
|
10
11
|
import AccordionSections from './AccordionSections';
|
|
@@ -18,7 +19,7 @@ import { Image } from './FormFieldTypes/Image';
|
|
|
18
19
|
import ObjectPropertyInput from './FormFieldTypes/relatedObjectFiles/ObjectPropertyInput';
|
|
19
20
|
import UserProperty from './FormFieldTypes/UserProperty';
|
|
20
21
|
import FormSections from './FormSections';
|
|
21
|
-
import { entryIsVisible,
|
|
22
|
+
import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
|
|
22
23
|
function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors, validation) {
|
|
23
24
|
return {
|
|
24
25
|
inputId: entryId,
|
|
@@ -38,7 +39,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
|
|
|
38
39
|
}
|
|
39
40
|
export function RecursiveEntryRenderer(props) {
|
|
40
41
|
const { entry } = props;
|
|
41
|
-
const {
|
|
42
|
+
const { object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, width, } = useFormContext();
|
|
42
43
|
const { isBelow, breakpoints } = useWidgetSize({
|
|
43
44
|
scroll: false,
|
|
44
45
|
defaultWidth: width,
|
|
@@ -50,19 +51,41 @@ export function RecursiveEntryRenderer(props) {
|
|
|
50
51
|
const entryId = getEntryId(entry) || 'defaultId';
|
|
51
52
|
const display = 'display' in entry ? entry.display : undefined;
|
|
52
53
|
const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
|
|
53
|
-
const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
|
|
54
|
-
const middleObject = fetchedOptions[`${entryId}MiddleObject`];
|
|
55
54
|
const fieldDefinition = useMemo(() => {
|
|
56
55
|
if (!object)
|
|
57
56
|
return undefined;
|
|
58
57
|
return getFieldDefinition(entry, object, parameters);
|
|
59
58
|
}, [entry, parameters, object]);
|
|
60
59
|
const validation = fieldDefinition?.validation || {};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
});
|
|
66
89
|
if (associatedObject?.propertyId === entryId)
|
|
67
90
|
return null;
|
|
68
91
|
// If the entry is hidden, clear its value and any nested values, and skip rendering
|
|
@@ -103,12 +126,11 @@ export function RecursiveEntryRenderer(props) {
|
|
|
103
126
|
}
|
|
104
127
|
else if (fieldDefinition.type === 'collection') {
|
|
105
128
|
if (fieldDefinition?.manyToManyPropertyId) {
|
|
106
|
-
if (
|
|
107
|
-
return (
|
|
108
|
-
React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances:
|
|
109
|
-
initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: 'criteria' in validation && validation.criteria
|
|
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
|
|
110
132
|
? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
|
|
111
|
-
: undefined, hasDescription: !!display?.description })))
|
|
133
|
+
: undefined, hasDescription: !!display?.description })));
|
|
112
134
|
}
|
|
113
135
|
else {
|
|
114
136
|
// when in the builder preview, the middle object won't be fetched so instead show an empty field
|
|
@@ -138,7 +160,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
138
160
|
}
|
|
139
161
|
else if (fieldDefinition.type === 'document' || fieldDefinition.type === 'file') {
|
|
140
162
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
141
|
-
React.createElement(Document, { id: entryId, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
|
|
163
|
+
React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: !!errors?.[entryId], value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description, validate: validation })));
|
|
142
164
|
}
|
|
143
165
|
else if (fieldDefinition.type === 'criteria') {
|
|
144
166
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
@@ -12,9 +12,10 @@ export type FieldAddress = {
|
|
|
12
12
|
export type AccessCheck = {
|
|
13
13
|
result: boolean;
|
|
14
14
|
};
|
|
15
|
-
export type
|
|
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;
|
|
@@ -112,3 +113,7 @@ export type InstanceLink = {
|
|
|
112
113
|
id: string;
|
|
113
114
|
objectId: string;
|
|
114
115
|
};
|
|
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, Property, Section, Sections, UserAccount } 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 {
|
|
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;
|
|
@@ -41,7 +41,8 @@ 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
|
|
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[]>;
|
|
45
46
|
export declare const getErrorCountForSection: (section: Section | Column, errors?: FieldErrors) => number;
|
|
46
47
|
export declare const propertyToParameter: (property: Property) => InputParameter;
|
|
47
48
|
export declare const propertyValidationToParameterValidation: (property: Property) => InputParameter['validation'];
|
|
@@ -49,16 +50,13 @@ export declare const convertPropertiesToParams: (object: Obj) => InputParameter[
|
|
|
49
50
|
export declare function getUnnestedEntries(entries: FormEntry[]): FormEntry[];
|
|
50
51
|
export declare const isEmptyWithDefault: (fieldValue: unknown, entry: InputParameterReference | InputField, instance: Record<string, unknown> | object) => boolean | "" | 0 | undefined;
|
|
51
52
|
export declare const docProperties: Property[];
|
|
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[]>;
|
|
53
|
+
export declare const uploadFiles: (files: (File | DocumentReference)[], apiServices: ApiServices, actionId?: string, linkTo?: InstanceLink, shortCircuit?: boolean) => Promise<FileUploadBatchResult>;
|
|
56
54
|
/**
|
|
57
55
|
* Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
|
|
58
56
|
* This is used after instance creation when the instance ID becomes available
|
|
59
57
|
*/
|
|
60
|
-
export declare const createFileLinks: (
|
|
61
|
-
export declare const uploadDocuments: (files: (File |
|
|
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[]>;
|
|
62
60
|
export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
63
61
|
showAlert: boolean;
|
|
64
62
|
message?: string;
|
|
@@ -87,7 +85,7 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
|
|
|
87
85
|
objectId?: string;
|
|
88
86
|
}, parameters?: InputParameter[]): Promise<FieldValues>;
|
|
89
87
|
export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
|
|
90
|
-
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[];
|
|
88
|
+
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
|
|
91
89
|
/**
|
|
92
90
|
* Converts a plain text string to RTF format suitable for a RichTextEditor.
|
|
93
91
|
*
|
|
@@ -101,5 +99,6 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], obj
|
|
|
101
99
|
* This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
|
|
102
100
|
*/
|
|
103
101
|
export declare function plainTextToRtf(plainText: string): string;
|
|
104
|
-
export declare function getFieldDefinition(entry: FormEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
|
|
102
|
+
export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
|
|
105
103
|
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,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';
|
|
@@ -127,7 +128,15 @@ const getEntryType = (entry, parameters) => {
|
|
|
127
128
|
};
|
|
128
129
|
export function getPrefixedUrl(url) {
|
|
129
130
|
const wcsMatchers = ['/apps', '/pages', '/widgets'];
|
|
130
|
-
const dataMatchers = [
|
|
131
|
+
const dataMatchers = [
|
|
132
|
+
'/objects',
|
|
133
|
+
'/correspondenceTemplates',
|
|
134
|
+
'/documents',
|
|
135
|
+
'/payments',
|
|
136
|
+
'/forms',
|
|
137
|
+
'/locations',
|
|
138
|
+
'/files',
|
|
139
|
+
];
|
|
131
140
|
const signalrMatchers = ['/hubs'];
|
|
132
141
|
const accessManagementMatchers = ['/users'];
|
|
133
142
|
const workflowMatchers = ['/workflows'];
|
|
@@ -342,31 +351,19 @@ export function updateCriteriaInputs(criteria, data, user, instance) {
|
|
|
342
351
|
},
|
|
343
352
|
}));
|
|
344
353
|
}
|
|
345
|
-
export async
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const fetchedInitialMiddleObjectInstances = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
|
|
359
|
-
params: { filter: JSON.stringify(filter) },
|
|
360
|
-
});
|
|
361
|
-
setFetchedOptions({
|
|
362
|
-
[`${fieldDefinition.id}InitialMiddleObjectInstances`]: fetchedInitialMiddleObjectInstances,
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
console.error('Error fetching collection data:', error);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
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
|
+
};
|
|
370
367
|
export const getErrorCountForSection = (section, errors) => {
|
|
371
368
|
const entries = section.entries || [];
|
|
372
369
|
return isArray(section.entries)
|
|
@@ -479,53 +476,90 @@ export const docProperties = [
|
|
|
479
476
|
type: 'string',
|
|
480
477
|
},
|
|
481
478
|
];
|
|
482
|
-
|
|
483
|
-
* Upload files using the POST /files endpoint for sys__file objects
|
|
484
|
-
*/
|
|
485
|
-
export const uploadFiles = async (files, apiServices, actionId = '_create', metadata, linkTo) => {
|
|
479
|
+
export const uploadFiles = async (files, apiServices, actionId = '_create', linkTo, shortCircuit = true) => {
|
|
486
480
|
// Separate already uploaded files from files that need uploading
|
|
487
481
|
const alreadyUploaded = files.filter((file) => !('size' in file));
|
|
488
482
|
const filesToUpload = files.filter((file) => 'size' in file);
|
|
489
|
-
|
|
490
|
-
|
|
483
|
+
let failedUpload = false;
|
|
484
|
+
// Upload all files in parallel, handling each result individually
|
|
485
|
+
const uploadPromises = [];
|
|
486
|
+
for (const file of filesToUpload) {
|
|
491
487
|
const formData = new FormData();
|
|
492
488
|
formData.append('file', file);
|
|
493
489
|
formData.append('actionId', actionId);
|
|
494
490
|
formData.append('objectId', 'sys__file');
|
|
495
|
-
|
|
496
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
497
|
-
formData.append(key, value);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
491
|
+
formData.append('input', JSON.stringify({ name: file.name, contentType: file.type }));
|
|
500
492
|
if (linkTo) {
|
|
501
493
|
formData.append('linkTo', JSON.stringify(linkTo));
|
|
502
494
|
}
|
|
503
|
-
const
|
|
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;
|
|
504
518
|
return {
|
|
505
|
-
|
|
506
|
-
|
|
519
|
+
successfulUploads: [...alreadyUploaded, ...uploadedFiles],
|
|
520
|
+
errorMessage: failedCount > 0 ? `Failed to upload ${failedCount} file(s)` : undefined,
|
|
507
521
|
};
|
|
508
|
-
}
|
|
522
|
+
}
|
|
509
523
|
const uploadedFiles = await Promise.all(uploadPromises);
|
|
510
|
-
|
|
524
|
+
if (failedUpload) {
|
|
525
|
+
return {
|
|
526
|
+
successfulUploads: [],
|
|
527
|
+
errorMessage: 'An error occurred when uploading files',
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
successfulUploads: [...alreadyUploaded, ...uploadedFiles],
|
|
532
|
+
};
|
|
511
533
|
};
|
|
512
534
|
/**
|
|
513
535
|
* Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
|
|
514
536
|
* This is used after instance creation when the instance ID becomes available
|
|
515
537
|
*/
|
|
516
|
-
export const createFileLinks = async (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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);
|
|
529
563
|
};
|
|
530
564
|
export const uploadDocuments = async (files, metadata, apiServices, instanceId, objectId) => {
|
|
531
565
|
const allDocuments = [];
|
|
@@ -553,20 +587,29 @@ export const uploadDocuments = async (files, metadata, apiServices, instanceId,
|
|
|
553
587
|
export const deleteDocuments = async (submittedFields, requestSuccess, apiServices, object, instance, action, setSnackbarError) => {
|
|
554
588
|
const documentProperties = action?.parameters
|
|
555
589
|
? action.parameters.filter((param) => ['document', 'file'].includes(param.type))
|
|
556
|
-
: object
|
|
590
|
+
: object.properties?.filter((prop) => ['document', 'file'].includes(prop.type));
|
|
557
591
|
for (const docProperty of documentProperties ?? []) {
|
|
558
592
|
const savedValue = submittedFields[docProperty.id];
|
|
559
|
-
const originalValue = instance
|
|
593
|
+
const originalValue = instance[docProperty.id];
|
|
560
594
|
const documentsToRemove = requestSuccess
|
|
561
595
|
? (originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? [])
|
|
562
596
|
: (savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? []);
|
|
563
597
|
for (const doc of documentsToRemove) {
|
|
564
598
|
try {
|
|
565
|
-
//
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
+
}
|
|
570
613
|
}
|
|
571
614
|
catch (error) {
|
|
572
615
|
if (error) {
|
|
@@ -581,6 +624,28 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
|
|
|
581
624
|
}
|
|
582
625
|
}
|
|
583
626
|
};
|
|
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
|
+
}
|
|
584
649
|
/**
|
|
585
650
|
* Transforms a form submission into a format safe for API submission.
|
|
586
651
|
*
|
|
@@ -598,44 +663,44 @@ export async function formatSubmission(submission, apiServices, objectId, instan
|
|
|
598
663
|
if (associatedObject) {
|
|
599
664
|
delete submission[associatedObject.propertyId];
|
|
600
665
|
}
|
|
601
|
-
const allEntries = getUnnestedEntries(form?.entries ?? [])
|
|
666
|
+
const allEntries = getUnnestedEntries(form?.entries ?? []);
|
|
602
667
|
for (const [key, value] of Object.entries(submission)) {
|
|
603
668
|
const entry = allEntries?.find((entry) => getEntryId(entry) === key);
|
|
604
669
|
if (isArray(value)) {
|
|
605
|
-
|
|
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).
|
|
606
676
|
const fileInArray = value.some((item) => item instanceof File);
|
|
607
677
|
if (fileInArray && apiServices && objectId) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
// Only pass linkTo if instanceId exists (update action)
|
|
617
|
-
instanceId ? { id: instanceId, objectId } : undefined);
|
|
618
|
-
}
|
|
619
|
-
else if (propertyType === 'document' && instanceId) {
|
|
620
|
-
uploadedDocuments = await uploadDocuments(value, {
|
|
621
|
-
type: '',
|
|
622
|
-
view_permission: '',
|
|
623
|
-
...entry?.documentMetadata,
|
|
624
|
-
}, apiServices, instanceId, objectId);
|
|
625
|
-
}
|
|
626
|
-
submission[key] = uploadedDocuments;
|
|
627
|
-
}
|
|
628
|
-
catch (err) {
|
|
629
|
-
if (err) {
|
|
630
|
-
setSnackbarError &&
|
|
631
|
-
setSnackbarError({
|
|
632
|
-
showAlert: true,
|
|
633
|
-
message: `An error occurred while uploading associated ${propertyType === 'file' ? 'files' : 'documents'}`,
|
|
634
|
-
isError: true,
|
|
635
|
-
});
|
|
636
|
-
}
|
|
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
|
+
});
|
|
637
686
|
return submission;
|
|
638
687
|
}
|
|
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 }));
|
|
691
|
+
}
|
|
692
|
+
}
|
|
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
|
+
}));
|
|
639
704
|
}
|
|
640
705
|
// if there are address fields with no value address needs to be set to undefined to be able to submit
|
|
641
706
|
}
|
|
@@ -882,3 +947,14 @@ function applyMaskToObfuscatedValue(value, mask) {
|
|
|
882
947
|
}
|
|
883
948
|
return maskedValue;
|
|
884
949
|
}
|
|
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
|
+
}
|