@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.
Files changed (54) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +4 -8
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +144 -238
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +67 -189
  4. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
  5. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +25 -12
  6. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +5 -4
  7. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +22 -34
  8. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +11 -2
  9. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +34 -6
  10. package/dist/published/components/custom/CriteriaBuilder/utils.js +89 -18
  11. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
  12. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +3 -6
  13. package/dist/published/components/custom/Form/utils.d.ts +2 -3
  14. package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
  15. package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +15 -7
  16. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +1 -1
  17. package/dist/published/components/custom/FormField/Select/Select.js +1 -1
  18. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -2
  19. package/dist/published/components/custom/FormV2/FormRenderer.js +7 -7
  20. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +0 -4
  21. package/dist/published/components/custom/FormV2/FormRendererContainer.js +49 -91
  22. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +0 -1
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +0 -1
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +3 -0
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +17 -44
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +2 -3
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +12 -44
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +3 -4
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +29 -41
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -0
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +2 -0
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +0 -14
  33. package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +6 -0
  34. package/dist/published/components/custom/FormV2/components/FormletRenderer.js +30 -0
  35. package/dist/published/components/custom/FormV2/components/HtmlView.js +12 -9
  36. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +13 -19
  37. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -6
  38. package/dist/published/components/custom/FormV2/components/utils.d.ts +14 -12
  39. package/dist/published/components/custom/FormV2/components/utils.js +123 -159
  40. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -5
  41. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +279 -35
  42. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +1 -6
  43. package/dist/published/components/custom/index.d.ts +0 -1
  44. package/dist/published/index.d.ts +1 -1
  45. package/dist/published/stories/CriteriaBuilder.stories.js +22 -70
  46. package/dist/published/stories/FormRenderer.stories.d.ts +3 -6
  47. package/dist/published/stories/FormRendererContainer.stories.d.ts +0 -20
  48. package/dist/published/stories/FormRendererData.d.ts +15 -0
  49. package/dist/published/stories/FormRendererData.js +63 -0
  50. package/dist/published/stories/sharedMswHandlers.js +4 -2
  51. package/dist/published/theme/hooks.d.ts +0 -1
  52. package/package.json +2 -3
  53. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +0 -12
  54. 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, ObjWithRoot, PanelViewEntry, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
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 { DocumentReference, FileUploadBatchResult, InstanceLink } 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;
@@ -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
- export declare const uploadFiles: (files: (File | DocumentReference)[], apiServices: ApiServices, actionId?: string, fileObjectId?: string, linkTo?: InstanceLink, shortCircuit?: boolean) => Promise<FileUploadBatchResult>;
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: (fileReferences: DocumentReference[], linkedInstance: InstanceLink, apiServices: ApiServices) => Promise<void>;
59
- export declare const uploadDocuments: (files: (File | DocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<DocumentReference[]>;
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: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
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: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
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
- 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
- }
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
- export const uploadFiles = async (files, apiServices, actionId = '_create', fileObjectId = 'sys__file', linkTo, shortCircuit = true) => {
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
- let failedUpload = false;
485
- // Upload all files in parallel, handling each result individually
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', fileObjectId);
492
- formData.append('input', JSON.stringify({ name: file.name, contentType: file.type }));
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 uploadPromise = (async () => {
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
- successfulUploads: [...alreadyUploaded, ...uploadedFiles],
521
- errorMessage: failedCount > 0 ? `Failed to upload ${failedCount} file(s)` : undefined,
535
+ id: fileInstance.id,
536
+ name: fileInstance.name,
522
537
  };
523
- }
538
+ });
524
539
  const uploadedFiles = await Promise.all(uploadPromises);
525
- if (failedUpload) {
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 (fileReferences, linkedInstance, apiServices) => {
540
- // Link files in parallel, handling each result individually
541
- const linkPromises = [];
542
- for (const file of fileReferences) {
543
- const linkPromise = (async () => {
544
- try {
545
- await apiServices.post(getPrefixedUrl(`/objects/sys__fileLink/instances/actions`), {
546
- actionId: '_create',
547
- input: { name: 'File Link', file: { id: file.id, name: file.name }, linkedInstance },
548
- });
549
- }
550
- catch (error) {
551
- console.error(`Failed to create file link for ${file.name}:`, error);
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.properties?.filter((prop) => ['document', 'file'].includes(prop.type));
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
- // Build context for state model
596
- const fieldType = docProperty.type === 'file' ? 'file' : 'document';
597
- if (fieldType === 'file') {
598
- // For file properties, unlink the file. Don't delete the actual file
599
- // since other instances may be using it.
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
- async function handleUploads(files, propertyType, entry, apiServices, objectId, instanceId) {
624
- if (propertyType === 'file') {
625
- const fileObjectId = entry.display?.fileObjectId ?? 'sys__file';
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 (propertyType === 'document' && instanceId) {
630
- try {
631
- const docs = await uploadDocuments(files, {
632
- type: '',
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
- return { successfulUploads: [] };
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
- const propertyType = getEntryType(entry, parameters);
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
- const result = await handleUploads(value, propertyType ?? '', entry, apiServices, objectId, instanceId);
675
- if (result.errorMessage) {
676
- setSnackbarError?.({
677
- showAlert: true,
678
- // Provide generic message since we're ignoring a partial upload.
679
- message: `An error occurred while uploading associated ${propertyType === 'file' ? 'files' : 'documents'}`,
680
- isError: true,
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
- (entry.display?.relatedObjectId ||
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.properties?.find((prop) => prop.id === entry.propertyId.split('.')[0])
860
- : object.properties?.find((prop) => prop.id === entry.propertyId);
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
  });