@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.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +8 -4
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +238 -141
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +189 -67
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +12 -25
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +4 -5
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +34 -22
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +2 -11
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +6 -34
- package/dist/published/components/custom/CriteriaBuilder/utils.js +18 -89
- 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/Form/utils.d.ts +1 -0
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +3 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +9 -4
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +5 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +116 -53
- package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Body.js +4 -2
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +46 -17
- 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 +44 -11
- 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 +41 -29
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.js +6 -14
- package/dist/published/components/custom/FormV2/components/HtmlView.js +4 -0
- package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.js +15 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +18 -19
- package/dist/published/components/custom/FormV2/components/types.d.ts +8 -2
- package/dist/published/components/custom/FormV2/components/utils.d.ts +11 -8
- package/dist/published/components/custom/FormV2/components/utils.js +194 -78
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +180 -2
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +212 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +26 -10
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +1 -0
- package/dist/published/components/custom/index.d.ts +1 -0
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
- package/dist/published/stories/FormRenderer.stories.d.ts +9 -3
- package/dist/published/stories/FormRenderer.stories.js +1 -0
- package/dist/published/stories/FormRendererContainer.stories.d.ts +25 -0
- package/dist/published/theme/hooks.d.ts +1 -0
- 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
|
|
7
|
-
import
|
|
8
|
-
import
|
|
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(
|
|
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,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
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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, {
|
|
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),
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
@@ -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: (
|
|
66
|
-
export declare const uploadDocuments: (files: (File |
|
|
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
|
-
|
|
520
|
-
|
|
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',
|
|
525
|
-
|
|
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
|
|
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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
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 (
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
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 (
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
}
|