@evoke-platform/ui-components 1.12.0 → 1.13.0-dev.1
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/FormV2/FormRendererContainer.js +20 -2
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +8 -4
- package/dist/published/components/custom/FormV2/components/types.d.ts +4 -0
- package/dist/published/components/custom/FormV2/components/utils.d.ts +8 -3
- package/dist/published/components/custom/FormV2/components/utils.js +35 -8
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +1 -1
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import { Box } from '../../layout';
|
|
|
7
7
|
import ErrorComponent from '../ErrorComponent';
|
|
8
8
|
import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
|
|
9
9
|
import Header from './components/Header';
|
|
10
|
-
import { convertPropertiesToParams, deleteDocuments, encodePageSlug, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, } from './components/utils';
|
|
10
|
+
import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, } from './components/utils';
|
|
11
11
|
import FormRenderer from './FormRenderer';
|
|
12
12
|
function FormRendererContainer(props) {
|
|
13
13
|
const { instanceId, pageNavigation, dataType, display, formId, objectId, actionId, richTextEditor, onSubmit, onDiscardChanges: onDiscardChangesOverride, associatedObject, renderContainer, onSubmitError, sx, renderHeader, renderBody, renderFooter, } = props;
|
|
@@ -167,9 +167,25 @@ function FormRendererContainer(props) {
|
|
|
167
167
|
}
|
|
168
168
|
setInstance(updatedInstance);
|
|
169
169
|
};
|
|
170
|
+
const linkFiles = async (submission, linkTo) => {
|
|
171
|
+
// Create file links for any uploaded files after instance creation
|
|
172
|
+
for (const property of sanitizedObject?.properties?.filter((property) => property.type === 'file') ?? []) {
|
|
173
|
+
const files = submission[property.id];
|
|
174
|
+
if (files?.length) {
|
|
175
|
+
try {
|
|
176
|
+
await createFileLinks(files, linkTo, apiServices);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error('Failed to create file links:', error);
|
|
180
|
+
// Don't fail the entire submission if file linking fails
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
170
185
|
const saveHandler = async (submission) => {
|
|
171
|
-
if (!form)
|
|
186
|
+
if (!form) {
|
|
172
187
|
return;
|
|
188
|
+
}
|
|
173
189
|
submission = await formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError);
|
|
174
190
|
try {
|
|
175
191
|
if (action?.type === 'create') {
|
|
@@ -180,6 +196,8 @@ function FormRendererContainer(props) {
|
|
|
180
196
|
.map((property) => property.id) ?? []),
|
|
181
197
|
});
|
|
182
198
|
if (response) {
|
|
199
|
+
// Manually link files to created instance.
|
|
200
|
+
await linkFiles(submission, { id: response.id, objectId: form.objectId });
|
|
183
201
|
onSubmissionSuccess(response);
|
|
184
202
|
}
|
|
185
203
|
}
|
|
@@ -85,8 +85,8 @@ export function RecursiveEntryRenderer(props) {
|
|
|
85
85
|
}
|
|
86
86
|
else if (fieldDefinition.type === 'object') {
|
|
87
87
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
88
|
-
React.createElement(ObjectPropertyInput, { relatedObjectId: !fieldDefinition.objectId ? display?.relatedObjectId : fieldDefinition.objectId, fieldDefinition: fieldDefinition, id: entryId, mode: display?.mode || 'default', error: !!errors?.[entryId], displayOption: display?.relatedObjectDisplay || 'dialogBox', initialValue: fieldValue, readOnly: entry.type === 'readonlyField', filter: validation
|
|
89
|
-
? updateCriteriaInputs(validation.criteria, getValues(), userAccount)
|
|
88
|
+
React.createElement(ObjectPropertyInput, { relatedObjectId: !fieldDefinition.objectId ? display?.relatedObjectId : fieldDefinition.objectId, fieldDefinition: fieldDefinition, id: entryId, mode: display?.mode || 'default', error: !!errors?.[entryId], displayOption: display?.relatedObjectDisplay || 'dialogBox', initialValue: fieldValue, readOnly: entry.type === 'readonlyField', filter: 'criteria' in validation && validation.criteria
|
|
89
|
+
? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
|
|
90
90
|
: undefined, sortBy: typeof display?.defaultValue === 'object' && 'sortBy' in display.defaultValue
|
|
91
91
|
? display?.defaultValue.sortBy
|
|
92
92
|
: undefined, orderBy: typeof display?.defaultValue === 'object' && 'orderBy' in display.defaultValue
|
|
@@ -104,7 +104,9 @@ export function RecursiveEntryRenderer(props) {
|
|
|
104
104
|
if (middleObject && !isEmpty(middleObject)) {
|
|
105
105
|
return (initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
106
106
|
React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] ||
|
|
107
|
-
initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria:
|
|
107
|
+
initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: 'criteria' in validation && validation.criteria
|
|
108
|
+
? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
|
|
109
|
+
: undefined, hasDescription: !!display?.description }))));
|
|
108
110
|
}
|
|
109
111
|
else {
|
|
110
112
|
// when in the builder preview, the middle object won't be fetched so instead show an empty field
|
|
@@ -116,7 +118,9 @@ export function RecursiveEntryRenderer(props) {
|
|
|
116
118
|
}
|
|
117
119
|
else {
|
|
118
120
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
119
|
-
React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria:
|
|
121
|
+
React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: 'criteria' in validation && validation.criteria
|
|
122
|
+
? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
|
|
123
|
+
: undefined, viewLayout: display?.viewLayout, entry: entry })));
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
else if (fieldDefinition.type === 'richText') {
|
|
@@ -4,7 +4,7 @@ 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 { SavedDocumentReference } from './types';
|
|
7
|
+
import { InstanceLink, SavedDocumentReference } from './types';
|
|
8
8
|
export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
|
|
9
9
|
export declare const normalizeDateTime: (dateTime: LocalDateTime) => string;
|
|
10
10
|
export declare function isAddressProperty(key: string): boolean;
|
|
@@ -40,7 +40,7 @@ export declare const encodePageSlug: (slug: string) => string;
|
|
|
40
40
|
export declare function getDefaultPages(parameters: InputParameter[], defaultPages: Record<string, string> | undefined, findDefaultPageSlugFor: (objectId: string) => Promise<string | undefined>): Promise<{
|
|
41
41
|
[x: string]: string;
|
|
42
42
|
}>;
|
|
43
|
-
export declare function updateCriteriaInputs(criteria: Record<string, unknown>, data: Record<string, unknown>, user?: UserAccount): Record<string, unknown>;
|
|
43
|
+
export declare function updateCriteriaInputs(criteria: Record<string, unknown>, data: Record<string, unknown>, user?: UserAccount, instance?: Record<string, unknown>): Record<string, unknown>;
|
|
44
44
|
export declare function fetchCollectionData(apiServices: ApiServices, fieldDefinition: InputParameter | Property, setFetchedOptions: (newData: FieldValues) => void, instanceId?: string, fetchedOptions?: Record<string, unknown>, initialMiddleObjectInstances?: ObjectInstance[]): Promise<void>;
|
|
45
45
|
export declare const getErrorCountForSection: (section: Section | Column, errors?: FieldErrors) => number;
|
|
46
46
|
export declare const propertyToParameter: (property: Property) => InputParameter;
|
|
@@ -52,7 +52,12 @@ export declare const docProperties: Property[];
|
|
|
52
52
|
/**
|
|
53
53
|
* Upload files using the POST /files endpoint for sys__file objects
|
|
54
54
|
*/
|
|
55
|
-
export declare const uploadFiles: (files: (File | SavedDocumentReference)[], apiServices: ApiServices, actionId?: string, metadata?: Record<string, string
|
|
55
|
+
export declare const uploadFiles: (files: (File | SavedDocumentReference)[], apiServices: ApiServices, actionId?: string, metadata?: Record<string, string>, linkTo?: InstanceLink) => Promise<SavedDocumentReference[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
|
|
58
|
+
* This is used after instance creation when the instance ID becomes available
|
|
59
|
+
*/
|
|
60
|
+
export declare const createFileLinks: (files: SavedDocumentReference[], linkedInstance: InstanceLink, apiServices: ApiServices) => Promise<void>;
|
|
56
61
|
export declare const uploadDocuments: (files: (File | SavedDocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<SavedDocumentReference[]>;
|
|
57
62
|
export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
58
63
|
showAlert: boolean;
|
|
@@ -307,12 +307,13 @@ function compileQueryValues(query, data) {
|
|
|
307
307
|
return query;
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
|
-
export function updateCriteriaInputs(criteria, data, user) {
|
|
310
|
+
export function updateCriteriaInputs(criteria, data, user, instance) {
|
|
311
311
|
const dataSet = {
|
|
312
312
|
input: {
|
|
313
313
|
...data,
|
|
314
314
|
},
|
|
315
315
|
user: user,
|
|
316
|
+
instance,
|
|
316
317
|
};
|
|
317
318
|
const compiledQuery = compileQueryValues(parseMongoDB(criteria), dataSet);
|
|
318
319
|
// The "compiledQueryValues" function filters out rules that have a value of "undefined".
|
|
@@ -471,7 +472,7 @@ export const docProperties = [
|
|
|
471
472
|
/**
|
|
472
473
|
* Upload files using the POST /files endpoint for sys__file objects
|
|
473
474
|
*/
|
|
474
|
-
export const uploadFiles = async (files, apiServices, actionId = '_create', metadata) => {
|
|
475
|
+
export const uploadFiles = async (files, apiServices, actionId = '_create', metadata, linkTo) => {
|
|
475
476
|
// Separate already uploaded files from files that need uploading
|
|
476
477
|
const alreadyUploaded = files.filter((file) => !('size' in file));
|
|
477
478
|
const filesToUpload = files.filter((file) => 'size' in file);
|
|
@@ -486,6 +487,9 @@ export const uploadFiles = async (files, apiServices, actionId = '_create', meta
|
|
|
486
487
|
formData.append(key, value);
|
|
487
488
|
}
|
|
488
489
|
}
|
|
490
|
+
if (linkTo) {
|
|
491
|
+
formData.append('linkTo', JSON.stringify(linkTo));
|
|
492
|
+
}
|
|
489
493
|
const fileInstance = await apiServices.post(getPrefixedUrl(`/files`), formData);
|
|
490
494
|
return {
|
|
491
495
|
id: fileInstance.id,
|
|
@@ -495,6 +499,24 @@ export const uploadFiles = async (files, apiServices, actionId = '_create', meta
|
|
|
495
499
|
const uploadedFiles = await Promise.all(uploadPromises);
|
|
496
500
|
return [...alreadyUploaded, ...uploadedFiles];
|
|
497
501
|
};
|
|
502
|
+
/**
|
|
503
|
+
* Creates file links for uploaded files by calling the objects endpoint with sys__fileLink
|
|
504
|
+
* This is used after instance creation when the instance ID becomes available
|
|
505
|
+
*/
|
|
506
|
+
export const createFileLinks = async (files, linkedInstance, apiServices) => {
|
|
507
|
+
const linkPromises = files.map(async (file) => {
|
|
508
|
+
await apiServices.post(getPrefixedUrl(`/objects/sys__fileLink/instances`), {
|
|
509
|
+
name: 'File Link',
|
|
510
|
+
file: { id: file.id, name: file.name },
|
|
511
|
+
linkedInstance,
|
|
512
|
+
}, {
|
|
513
|
+
params: {
|
|
514
|
+
actionId: '_create',
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
await Promise.all(linkPromises);
|
|
519
|
+
};
|
|
498
520
|
export const uploadDocuments = async (files, metadata, apiServices, instanceId, objectId) => {
|
|
499
521
|
const allDocuments = [];
|
|
500
522
|
const formData = new FormData();
|
|
@@ -572,20 +594,25 @@ export async function formatSubmission(submission, apiServices, objectId, instan
|
|
|
572
594
|
if (isArray(value)) {
|
|
573
595
|
// Only upload if array contains File instances (not SavedDocumentReference)
|
|
574
596
|
const fileInArray = value.some((item) => item instanceof File);
|
|
575
|
-
if (fileInArray &&
|
|
597
|
+
if (fileInArray && apiServices && objectId) {
|
|
576
598
|
// Determine property type from the entry
|
|
577
599
|
const propertyType = entry?.input?.type;
|
|
578
600
|
try {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
601
|
+
let uploadedDocuments = [];
|
|
602
|
+
if (propertyType === 'file') {
|
|
603
|
+
uploadedDocuments = await uploadFiles(value, apiServices, '_create', {
|
|
582
604
|
...entry?.documentMetadata,
|
|
583
|
-
}
|
|
584
|
-
|
|
605
|
+
},
|
|
606
|
+
// Only pass linkTo if instanceId exists (update action)
|
|
607
|
+
instanceId ? { id: instanceId, objectId } : undefined);
|
|
608
|
+
}
|
|
609
|
+
else if (propertyType === 'document' && instanceId) {
|
|
610
|
+
uploadedDocuments = await uploadDocuments(value, {
|
|
585
611
|
type: '',
|
|
586
612
|
view_permission: '',
|
|
587
613
|
...entry?.documentMetadata,
|
|
588
614
|
}, apiServices, instanceId, objectId);
|
|
615
|
+
}
|
|
589
616
|
submission[key] = uploadedDocuments;
|
|
590
617
|
}
|
|
591
618
|
catch (err) {
|
|
@@ -1465,7 +1465,7 @@ describe('FormRenderer', () => {
|
|
|
1465
1465
|
const addButton = await screen.findByRole('button', { name: /add/i });
|
|
1466
1466
|
await user.click(addButton);
|
|
1467
1467
|
await screen.findByRole('dialog');
|
|
1468
|
-
const nameField = screen.
|
|
1468
|
+
const nameField = await screen.findByRole('textbox', { name: 'Name *' });
|
|
1469
1469
|
await user.type(nameField, 'New Collection Item');
|
|
1470
1470
|
const submitButton = screen.getByRole('button', { name: 'Create Collection Item' });
|
|
1471
1471
|
await user.click(submitButton);
|