@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.
@@ -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?.criteria
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: validation?.criteria, hasDescription: !!display?.description }))));
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: validation?.criteria, viewLayout: display?.viewLayout, entry: entry })));
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') {
@@ -108,3 +108,7 @@ export type SectionsProps = {
108
108
  export type RichTextFormEntry = FormEntry & {
109
109
  uniqueId: string;
110
110
  };
111
+ export type InstanceLink = {
112
+ id: string;
113
+ objectId: string;
114
+ };
@@ -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>) => Promise<SavedDocumentReference[]>;
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 && instanceId && apiServices && objectId) {
597
+ if (fileInArray && apiServices && objectId) {
576
598
  // Determine property type from the entry
577
599
  const propertyType = entry?.input?.type;
578
600
  try {
579
- // Use uploadFiles for 'file' type, uploadDocuments for 'document' type
580
- const uploadedDocuments = propertyType === 'file'
581
- ? await uploadFiles(value, apiServices, '_create', {
601
+ let uploadedDocuments = [];
602
+ if (propertyType === 'file') {
603
+ uploadedDocuments = await uploadFiles(value, apiServices, '_create', {
582
604
  ...entry?.documentMetadata,
583
- })
584
- : await uploadDocuments(value, {
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.getByRole('textbox', { name: 'Name *' });
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.12.0",
3
+ "version": "1.13.0-dev.1",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",