@evoke-platform/ui-components 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +4 -8
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +144 -238
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +67 -189
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +25 -12
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +5 -4
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +22 -34
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +11 -2
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +34 -6
- package/dist/published/components/custom/CriteriaBuilder/utils.js +89 -18
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +3 -6
- package/dist/published/components/custom/Form/utils.d.ts +2 -3
- package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +15 -7
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/Select/Select.js +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -2
- package/dist/published/components/custom/FormV2/FormRenderer.js +7 -7
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +0 -4
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +49 -91
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +3 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +17 -44
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +12 -44
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +3 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +29 -41
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +2 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +0 -14
- package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +6 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.js +30 -0
- package/dist/published/components/custom/FormV2/components/HtmlView.js +12 -9
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +13 -19
- package/dist/published/components/custom/FormV2/components/types.d.ts +1 -6
- package/dist/published/components/custom/FormV2/components/utils.d.ts +14 -12
- package/dist/published/components/custom/FormV2/components/utils.js +123 -159
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -5
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +279 -35
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +1 -6
- package/dist/published/components/custom/index.d.ts +0 -1
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +22 -70
- package/dist/published/stories/FormRenderer.stories.d.ts +3 -6
- package/dist/published/stories/FormRendererContainer.stories.d.ts +0 -20
- package/dist/published/stories/FormRendererData.d.ts +15 -0
- package/dist/published/stories/FormRendererData.js +63 -0
- package/dist/published/stories/sharedMswHandlers.js +4 -2
- package/dist/published/theme/hooks.d.ts +0 -1
- package/package.json +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +0 -12
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +0 -197
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance,
|
|
2
|
+
import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount, ObjWithRoot, PanelViewEntry } 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 { InstanceLink, SavedDocumentReference } from './types';
|
|
8
8
|
export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
|
|
9
9
|
export declare const normalizeDateTime: (dateTime: LocalDateTime) => string;
|
|
10
10
|
export declare function isAddressProperty(key: string): boolean;
|
|
@@ -20,6 +20,10 @@ export declare const entryIsVisible: (entry: FormEntry, instance?: FieldValues,
|
|
|
20
20
|
*/
|
|
21
21
|
export declare const getNestedParameterIds: (entry: Sections | Columns) => string[];
|
|
22
22
|
export declare const getEntryId: (entry: FormEntry) => string | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Returns editable field IDs that are currently visible on the form.
|
|
25
|
+
*/
|
|
26
|
+
export declare const getVisibleEditableFieldIds: (entries: FormEntry[], instance?: FieldValues, formValues?: FieldValues) => string[];
|
|
23
27
|
export declare function getPrefixedUrl(url: string): string;
|
|
24
28
|
export declare const isOptionEqualToValue: (option: AutocompleteOption | string, value: unknown) => boolean;
|
|
25
29
|
export declare function addressProperties(addressProperty: Property): Property[];
|
|
@@ -50,13 +54,16 @@ export declare const convertPropertiesToParams: (object: Obj) => InputParameter[
|
|
|
50
54
|
export declare function getUnnestedEntries(entries: FormEntry[]): FormEntry[];
|
|
51
55
|
export declare const isEmptyWithDefault: (fieldValue: unknown, entry: InputParameterReference | InputField, instance: Record<string, unknown> | object) => boolean | "" | 0 | undefined;
|
|
52
56
|
export declare const docProperties: Property[];
|
|
53
|
-
|
|
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[]>;
|
|
54
61
|
/**
|
|
55
62
|
* Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
|
|
56
63
|
* This is used after instance creation when the instance ID becomes available
|
|
57
64
|
*/
|
|
58
|
-
export declare const createFileLinks: (
|
|
59
|
-
export declare const uploadDocuments: (files: (File |
|
|
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[]>;
|
|
60
67
|
export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
61
68
|
showAlert: boolean;
|
|
62
69
|
message?: string;
|
|
@@ -82,10 +89,9 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
|
|
|
82
89
|
}>>, associatedObject?: {
|
|
83
90
|
instanceId: string;
|
|
84
91
|
propertyId: string;
|
|
85
|
-
objectId?: string;
|
|
86
92
|
}, parameters?: InputParameter[]): Promise<FieldValues>;
|
|
87
93
|
export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
|
|
88
|
-
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object
|
|
94
|
+
export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object?: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
|
|
89
95
|
/**
|
|
90
96
|
* Converts a plain text string to RTF format suitable for a RichTextEditor.
|
|
91
97
|
*
|
|
@@ -99,12 +105,8 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | Pa
|
|
|
99
105
|
* This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
|
|
100
106
|
*/
|
|
101
107
|
export declare function plainTextToRtf(plainText: string): string;
|
|
102
|
-
export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object
|
|
108
|
+
export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object?: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
|
|
103
109
|
export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
|
|
104
|
-
export declare function handleFileUpload(apiServices: ApiServices, submission: FieldValues, actionId: string, objectId?: string, instanceId?: string, linkTo?: {
|
|
105
|
-
instanceId: string;
|
|
106
|
-
objectId: string;
|
|
107
|
-
}): Promise<ObjectInstance>;
|
|
108
110
|
export declare function useFormById(formId: string, apiServices: ApiServices, errorMessage?: string): import("@tanstack/react-query/build/legacy/types").UseQueryResult<EvokeForm, Error>;
|
|
109
111
|
/**
|
|
110
112
|
* Extract all values from a criteria/filter object.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { LocalDateTime } from '@js-joda/core';
|
|
2
|
-
import { useQuery } from '@tanstack/react-query';
|
|
3
2
|
import { jsonLogic } from 'json-logic-js-graphql';
|
|
4
3
|
import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } from 'lodash';
|
|
5
4
|
import { DateTime } from 'luxon';
|
|
@@ -7,6 +6,7 @@ import { nanoid } from 'nanoid';
|
|
|
7
6
|
import Handlebars from 'no-eval-handlebars';
|
|
8
7
|
import { defaultRuleProcessorMongoDB, formatQuery } from 'react-querybuilder';
|
|
9
8
|
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,15 +117,47 @@ export const getEntryId = (entry) => {
|
|
|
117
117
|
? entry.input.id
|
|
118
118
|
: undefined;
|
|
119
119
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Returns editable field IDs that are currently visible on the form.
|
|
122
|
+
*/
|
|
123
|
+
export const getVisibleEditableFieldIds = (entries, instance, formValues) => {
|
|
124
|
+
const fieldIds = new Set();
|
|
125
|
+
const collectVisibleIds = (entriesToCheck) => {
|
|
126
|
+
entriesToCheck.forEach((entry) => {
|
|
127
|
+
if (!entryIsVisible(entry, instance, formValues)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (entry.type === 'sections') {
|
|
131
|
+
entry.sections.forEach((section) => {
|
|
132
|
+
if (section.entries) {
|
|
133
|
+
collectVisibleIds(section.entries);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (entry.type === 'columns') {
|
|
139
|
+
entry.columns.forEach((column) => {
|
|
140
|
+
if (column.entries) {
|
|
141
|
+
collectVisibleIds(column.entries);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (entry.type !== 'input' && entry.type !== 'inputField') {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Collection entries are handled by their own flow and are not part of autosave payloads.
|
|
150
|
+
if (entry.type === 'inputField' && entry.input.type === 'collection') {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const fieldId = getEntryId(entry);
|
|
154
|
+
if (fieldId) {
|
|
155
|
+
fieldIds.add(fieldId);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
collectVisibleIds(entries ?? []);
|
|
160
|
+
return Array.from(fieldIds);
|
|
129
161
|
};
|
|
130
162
|
export function getPrefixedUrl(url) {
|
|
131
163
|
const wcsMatchers = ['/apps', '/pages', '/widgets'];
|
|
@@ -135,8 +167,8 @@ export function getPrefixedUrl(url) {
|
|
|
135
167
|
'/documents',
|
|
136
168
|
'/payments',
|
|
137
169
|
'/forms',
|
|
170
|
+
'/formlets',
|
|
138
171
|
'/locations',
|
|
139
|
-
'/files',
|
|
140
172
|
];
|
|
141
173
|
const signalrMatchers = ['/hubs'];
|
|
142
174
|
const accessManagementMatchers = ['/users'];
|
|
@@ -477,85 +509,53 @@ export const docProperties = [
|
|
|
477
509
|
type: 'string',
|
|
478
510
|
},
|
|
479
511
|
];
|
|
480
|
-
|
|
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) => {
|
|
481
516
|
// Separate already uploaded files from files that need uploading
|
|
482
517
|
const alreadyUploaded = files.filter((file) => !('size' in file));
|
|
483
518
|
const filesToUpload = files.filter((file) => 'size' in file);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const uploadPromises = [];
|
|
487
|
-
for (const file of filesToUpload) {
|
|
519
|
+
// Upload all files in parallel
|
|
520
|
+
const uploadPromises = filesToUpload.map(async (file) => {
|
|
488
521
|
const formData = new FormData();
|
|
489
522
|
formData.append('file', file);
|
|
490
523
|
formData.append('actionId', actionId);
|
|
491
|
-
formData.append('objectId',
|
|
492
|
-
|
|
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
|
+
}
|
|
493
530
|
if (linkTo) {
|
|
494
531
|
formData.append('linkTo', JSON.stringify(linkTo));
|
|
495
532
|
}
|
|
496
|
-
const
|
|
497
|
-
try {
|
|
498
|
-
const fileInstance = await apiServices.post(getPrefixedUrl(`/files`), formData);
|
|
499
|
-
return {
|
|
500
|
-
id: fileInstance.id,
|
|
501
|
-
name: fileInstance.name,
|
|
502
|
-
unsaved: true,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
catch (error) {
|
|
506
|
-
console.error(`Failed to upload file ${file.name}:`, error);
|
|
507
|
-
failedUpload = true;
|
|
508
|
-
}
|
|
509
|
-
})();
|
|
510
|
-
uploadPromises.push(uploadPromise);
|
|
511
|
-
}
|
|
512
|
-
if (!shortCircuit) {
|
|
513
|
-
// Wait for all upload attempts to complete (successes and failures)
|
|
514
|
-
const uploadResults = await Promise.allSettled(uploadPromises);
|
|
515
|
-
const uploadedFiles = uploadResults
|
|
516
|
-
.filter((result) => result.status === 'fulfilled' && !!result.value)
|
|
517
|
-
.map((result) => result.value);
|
|
518
|
-
const failedCount = filesToUpload.length - uploadedFiles.length;
|
|
533
|
+
const fileInstance = await apiServices.post(getPrefixedUrl(`/files`), formData);
|
|
519
534
|
return {
|
|
520
|
-
|
|
521
|
-
|
|
535
|
+
id: fileInstance.id,
|
|
536
|
+
name: fileInstance.name,
|
|
522
537
|
};
|
|
523
|
-
}
|
|
538
|
+
});
|
|
524
539
|
const uploadedFiles = await Promise.all(uploadPromises);
|
|
525
|
-
|
|
526
|
-
return {
|
|
527
|
-
successfulUploads: [],
|
|
528
|
-
errorMessage: 'An error occurred when uploading files',
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
return {
|
|
532
|
-
successfulUploads: [...alreadyUploaded, ...uploadedFiles],
|
|
533
|
-
};
|
|
540
|
+
return [...alreadyUploaded, ...uploadedFiles];
|
|
534
541
|
};
|
|
535
542
|
/**
|
|
536
543
|
* Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
|
|
537
544
|
* This is used after instance creation when the instance ID becomes available
|
|
538
545
|
*/
|
|
539
|
-
export const createFileLinks = async (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// The file remains unlinked and can be retried later
|
|
553
|
-
}
|
|
554
|
-
})();
|
|
555
|
-
linkPromises.push(linkPromise);
|
|
556
|
-
}
|
|
557
|
-
// Wait for all linking attempts to complete (successes and failures)
|
|
558
|
-
await Promise.allSettled(linkPromises);
|
|
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);
|
|
559
559
|
};
|
|
560
560
|
export const uploadDocuments = async (files, metadata, apiServices, instanceId, objectId) => {
|
|
561
561
|
const allDocuments = [];
|
|
@@ -583,29 +583,20 @@ export const uploadDocuments = async (files, metadata, apiServices, instanceId,
|
|
|
583
583
|
export const deleteDocuments = async (submittedFields, requestSuccess, apiServices, object, instance, action, setSnackbarError) => {
|
|
584
584
|
const documentProperties = action?.parameters
|
|
585
585
|
? action.parameters.filter((param) => ['document', 'file'].includes(param.type))
|
|
586
|
-
: object
|
|
586
|
+
: object?.properties?.filter((prop) => ['document', 'file'].includes(prop.type));
|
|
587
587
|
for (const docProperty of documentProperties ?? []) {
|
|
588
588
|
const savedValue = submittedFields[docProperty.id];
|
|
589
|
-
const originalValue = instance[docProperty.id];
|
|
589
|
+
const originalValue = instance?.[docProperty.id];
|
|
590
590
|
const documentsToRemove = requestSuccess
|
|
591
591
|
? (originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? [])
|
|
592
592
|
: (savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? []);
|
|
593
593
|
for (const doc of documentsToRemove) {
|
|
594
594
|
try {
|
|
595
|
-
//
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
await apiServices.post(getPrefixedUrl(`/files/${doc.id}/unlinkInstance`), {
|
|
601
|
-
linkedInstanceId: instance.id,
|
|
602
|
-
linkedObjectId: object.id,
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
else {
|
|
606
|
-
// For document properties, delete the document
|
|
607
|
-
await apiServices.delete(getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/${doc.id}`));
|
|
608
|
-
}
|
|
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);
|
|
609
600
|
}
|
|
610
601
|
catch (error) {
|
|
611
602
|
if (error) {
|
|
@@ -620,28 +611,16 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
|
|
|
620
611
|
}
|
|
621
612
|
}
|
|
622
613
|
};
|
|
623
|
-
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
const createActionId = entry.display?.createActionId ?? '_create';
|
|
627
|
-
return await uploadFiles(files, apiServices, createActionId, fileObjectId, instanceId ? { id: instanceId, objectId } : undefined);
|
|
614
|
+
const getEntryType = (entry, parameters) => {
|
|
615
|
+
if (entry?.type === 'inputField') {
|
|
616
|
+
return entry.input.type;
|
|
628
617
|
}
|
|
629
|
-
else if (
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
view_permission: '',
|
|
634
|
-
...entry?.documentMetadata,
|
|
635
|
-
}, apiServices, instanceId, objectId);
|
|
636
|
-
return { successfulUploads: docs };
|
|
637
|
-
}
|
|
638
|
-
catch (error) {
|
|
639
|
-
console.error('Error uploading documents:', error);
|
|
640
|
-
return { successfulUploads: [], errorMessage: 'Error uploading documents' };
|
|
641
|
-
}
|
|
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;
|
|
642
622
|
}
|
|
643
|
-
|
|
644
|
-
}
|
|
623
|
+
};
|
|
645
624
|
/**
|
|
646
625
|
* Transforms a form submission into a format safe for API submission.
|
|
647
626
|
*
|
|
@@ -659,36 +638,44 @@ export async function formatSubmission(submission, apiServices, objectId, instan
|
|
|
659
638
|
if (associatedObject) {
|
|
660
639
|
delete submission[associatedObject.propertyId];
|
|
661
640
|
}
|
|
662
|
-
const allEntries = getUnnestedEntries(form?.entries ?? []);
|
|
641
|
+
const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
|
|
663
642
|
for (const [key, value] of Object.entries(submission)) {
|
|
664
643
|
const entry = allEntries?.find((entry) => getEntryId(entry) === key);
|
|
665
644
|
if (isArray(value)) {
|
|
666
|
-
|
|
667
|
-
// The only array types we need to handle specially are 'file' and 'document'.
|
|
668
|
-
if (propertyType !== 'file' && propertyType !== 'document') {
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
// Only upload if array contains File instances (not SavedDocumentReference).
|
|
645
|
+
// Only upload if array contains File instances (not SavedDocumentReference)
|
|
672
646
|
const fileInArray = value.some((item) => item instanceof File);
|
|
673
647
|
if (fileInArray && apiServices && objectId) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
+
}
|
|
682
677
|
return submission;
|
|
683
678
|
}
|
|
684
|
-
submission[key] = result.successfulUploads;
|
|
685
|
-
}
|
|
686
|
-
else {
|
|
687
|
-
submission[key] = value
|
|
688
|
-
// This should never happen but it's possible that the else branch
|
|
689
|
-
// is reached because either 'apiServices' or 'objectId' is undefined.
|
|
690
|
-
// If that's the case the submission will fail if we submit File blobs.
|
|
691
|
-
.filter((file) => !(file instanceof File));
|
|
692
679
|
}
|
|
693
680
|
// if there are address fields with no value address needs to be set to undefined to be able to submit
|
|
694
681
|
}
|
|
@@ -702,8 +689,7 @@ export async function formatSubmission(submission, apiServices, objectId, instan
|
|
|
702
689
|
submission[key] =
|
|
703
690
|
entry &&
|
|
704
691
|
['input', 'inputField'].includes(entry.type) &&
|
|
705
|
-
|
|
706
|
-
associatedObject?.objectId)
|
|
692
|
+
entry.display?.relatedObjectId
|
|
707
693
|
? pick(value, 'id', 'name', 'objectId')
|
|
708
694
|
: pick(value, 'id', 'name');
|
|
709
695
|
}
|
|
@@ -856,8 +842,8 @@ export function getFieldDefinition(entry, object, parameters) {
|
|
|
856
842
|
}
|
|
857
843
|
else if (entry.type === 'readonlyField') {
|
|
858
844
|
def = isAddressProperty(entry.propertyId)
|
|
859
|
-
? object
|
|
860
|
-
: object
|
|
845
|
+
? object?.properties?.find((prop) => prop.id === entry.propertyId.split('.')[0])
|
|
846
|
+
: object?.properties?.find((prop) => prop.id === entry.propertyId);
|
|
861
847
|
}
|
|
862
848
|
else if (entry.type === 'inputField') {
|
|
863
849
|
def = entry.input;
|
|
@@ -935,34 +921,12 @@ function applyMaskToObfuscatedValue(value, mask) {
|
|
|
935
921
|
}
|
|
936
922
|
return maskedValue;
|
|
937
923
|
}
|
|
938
|
-
export async function handleFileUpload(apiServices, submission, actionId, objectId, instanceId, linkTo) {
|
|
939
|
-
const formData = new FormData();
|
|
940
|
-
if (submission['content'] instanceof File && submission['content'].size !== 0) {
|
|
941
|
-
formData.append('file', submission['content']);
|
|
942
|
-
}
|
|
943
|
-
formData.append('objectId', objectId ?? 'sys__file');
|
|
944
|
-
formData.append('actionId', actionId);
|
|
945
|
-
delete submission['content'];
|
|
946
|
-
formData.append('input', JSON.stringify(submission));
|
|
947
|
-
if (instanceId) {
|
|
948
|
-
return await apiServices.patch(getPrefixedUrl(`/files/${instanceId}`), formData);
|
|
949
|
-
}
|
|
950
|
-
else {
|
|
951
|
-
if (linkTo) {
|
|
952
|
-
formData.append('linkTo', JSON.stringify({
|
|
953
|
-
id: linkTo?.instanceId,
|
|
954
|
-
objectId: linkTo?.objectId,
|
|
955
|
-
}));
|
|
956
|
-
}
|
|
957
|
-
return await apiServices.post(getPrefixedUrl(`/files`), formData);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
924
|
export function useFormById(formId, apiServices, errorMessage) {
|
|
961
925
|
return useQuery({
|
|
962
926
|
queryKey: ['form', formId],
|
|
963
927
|
enabled: formId !== '_auto_' && !!formId,
|
|
964
928
|
staleTime: Infinity,
|
|
965
|
-
queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}`)),
|
|
929
|
+
queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}/effective`)),
|
|
966
930
|
meta: {
|
|
967
931
|
errorMessage,
|
|
968
932
|
},
|
|
@@ -34,6 +34,11 @@ describe('FormRenderer', () => {
|
|
|
34
34
|
if (sanitizedVersion === 'true') {
|
|
35
35
|
return HttpResponse.json(specialtyObject);
|
|
36
36
|
}
|
|
37
|
+
}), http.get('/api/data/objects/formletTestObject/effective', (req) => {
|
|
38
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
39
|
+
if (sanitizedVersion === 'true') {
|
|
40
|
+
return HttpResponse.json({});
|
|
41
|
+
}
|
|
37
42
|
}), http.get('/api/data/objects/accessibility508Object/effective', (req) => {
|
|
38
43
|
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
39
44
|
if (sanitizedVersion === 'true') {
|
|
@@ -416,7 +421,7 @@ describe('FormRenderer', () => {
|
|
|
416
421
|
});
|
|
417
422
|
describe('when passed a regular related object entry', () => {
|
|
418
423
|
const setupTestMocks = (object, form, instances) => {
|
|
419
|
-
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
424
|
+
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}/effective`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
420
425
|
};
|
|
421
426
|
describe('when in table view', () => {
|
|
422
427
|
describe('when mode is existing records only', () => {
|
|
@@ -880,7 +885,7 @@ describe('FormRenderer', () => {
|
|
|
880
885
|
});
|
|
881
886
|
it('displays a not found error in record creation mode if a form could not be found', async () => {
|
|
882
887
|
const user = userEvent.setup();
|
|
883
|
-
server.use(http.get('/api/data/forms/specialtyTypeForm', () => {
|
|
888
|
+
server.use(http.get('/api/data/forms/specialtyTypeForm/effective', () => {
|
|
884
889
|
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
885
890
|
}));
|
|
886
891
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
@@ -1180,7 +1185,7 @@ describe('FormRenderer', () => {
|
|
|
1180
1185
|
});
|
|
1181
1186
|
describe('when passed a dynamic related object entry', () => {
|
|
1182
1187
|
const setupTestMocks = (object, form, instances) => {
|
|
1183
|
-
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
1188
|
+
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}/effective`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
|
|
1184
1189
|
};
|
|
1185
1190
|
const form = {
|
|
1186
1191
|
id: 'relatedObjectTestForm',
|
|
@@ -1294,7 +1299,7 @@ describe('FormRenderer', () => {
|
|
|
1294
1299
|
});
|
|
1295
1300
|
it('displays a not found error in record creation mode if a form could not be found', async () => {
|
|
1296
1301
|
const user = userEvent.setup();
|
|
1297
|
-
server.use(http.get('/api/data/forms/specialtyTypeForm', () => {
|
|
1302
|
+
server.use(http.get('/api/data/forms/specialtyTypeForm/effective', () => {
|
|
1298
1303
|
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
1299
1304
|
}));
|
|
1300
1305
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
@@ -1306,7 +1311,7 @@ describe('FormRenderer', () => {
|
|
|
1306
1311
|
});
|
|
1307
1312
|
describe('when passed a one-to-many collection entry', () => {
|
|
1308
1313
|
const setupTestMocks = (object, form, instances) => {
|
|
1309
|
-
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])), http.get(`/api/data/objects/${object.id}/instances/checkAccess`, () => HttpResponse.json({
|
|
1314
|
+
server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}/effective`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])), http.get(`/api/data/objects/${object.id}/instances/checkAccess`, () => HttpResponse.json({
|
|
1310
1315
|
result: true,
|
|
1311
1316
|
})));
|
|
1312
1317
|
};
|
|
@@ -1648,4 +1653,42 @@ describe('FormRenderer', () => {
|
|
|
1648
1653
|
expect(textField).toHaveValue('Test Input');
|
|
1649
1654
|
});
|
|
1650
1655
|
});
|
|
1656
|
+
describe('when passed a formlet entry', () => {
|
|
1657
|
+
const formlet = {
|
|
1658
|
+
id: 'formletId',
|
|
1659
|
+
name: 'Test Formlet',
|
|
1660
|
+
entries: [
|
|
1661
|
+
{
|
|
1662
|
+
type: 'inputField',
|
|
1663
|
+
input: {
|
|
1664
|
+
type: 'date',
|
|
1665
|
+
id: 'dateId2',
|
|
1666
|
+
},
|
|
1667
|
+
display: {
|
|
1668
|
+
label: 'Date 2',
|
|
1669
|
+
required: false,
|
|
1670
|
+
},
|
|
1671
|
+
},
|
|
1672
|
+
],
|
|
1673
|
+
};
|
|
1674
|
+
beforeEach(() => {
|
|
1675
|
+
server.use(http.get('/api/data/formlets/formletId', () => HttpResponse.json(formlet)));
|
|
1676
|
+
});
|
|
1677
|
+
it('should render formlet entry fields', async () => {
|
|
1678
|
+
const form = {
|
|
1679
|
+
id: 'formWithFormlet',
|
|
1680
|
+
name: 'Form With Formlet',
|
|
1681
|
+
entries: [
|
|
1682
|
+
{
|
|
1683
|
+
type: 'formlet',
|
|
1684
|
+
formletId: 'formletId',
|
|
1685
|
+
},
|
|
1686
|
+
],
|
|
1687
|
+
actionId: '_update',
|
|
1688
|
+
objectId: 'formletTestObject',
|
|
1689
|
+
};
|
|
1690
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1691
|
+
await screen.findByLabelText('Date 2');
|
|
1692
|
+
});
|
|
1693
|
+
});
|
|
1651
1694
|
});
|