@evoke-platform/ui-components 1.13.0-dev.6 → 1.13.0-dev.7

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 (26) hide show
  1. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
  2. package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
  3. package/dist/published/components/custom/FormV2/FormRendererContainer.js +70 -66
  4. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
  5. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
  6. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
  7. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
  8. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
  9. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +104 -184
  10. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +19 -36
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -20
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +95 -169
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +12 -6
  17. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
  18. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +38 -16
  19. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
  20. package/dist/published/components/custom/FormV2/components/utils.js +25 -25
  21. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +48 -15
  22. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +38 -46
  23. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
  24. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +37 -12
  25. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
  26. package/package.json +3 -2
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
2
+ import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, ObjWithRoot, PanelViewEntry, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
3
3
  import { LocalDateTime } from '@js-joda/core';
4
4
  import { FieldErrors, FieldValues } from 'react-hook-form';
5
5
  import { ObjectProperty } from '../../../../types';
@@ -41,7 +41,8 @@ export declare function getDefaultPages(parameters: InputParameter[], defaultPag
41
41
  [x: string]: string;
42
42
  }>;
43
43
  export declare function updateCriteriaInputs(criteria: Record<string, unknown>, data: Record<string, unknown>, user?: UserAccount, instance?: Record<string, unknown>): Record<string, unknown>;
44
- export declare function fetchCollectionData(apiServices: ApiServices, fieldDefinition: InputParameter | Property, setFetchedOptions: (newData: FieldValues) => void, instanceId?: string, fetchedOptions?: Record<string, unknown>, initialMiddleObjectInstances?: ObjectInstance[]): Promise<void>;
44
+ export declare const fetchMiddleObject: (fieldDefinition: InputParameter | Property, apiServices: ApiServices) => Promise<ObjWithRoot | undefined>;
45
+ export declare const fetchInitialMiddleObjectInstances: (apiServices: ApiServices, fieldDefinition: InputParameter | Property, instanceId: string) => Promise<ObjectInstance[]>;
45
46
  export declare const getErrorCountForSection: (section: Section | Column, errors?: FieldErrors) => number;
46
47
  export declare const propertyToParameter: (property: Property) => InputParameter;
47
48
  export declare const propertyValidationToParameterValidation: (property: Property) => InputParameter['validation'];
@@ -84,7 +85,7 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
84
85
  objectId?: string;
85
86
  }, parameters?: InputParameter[]): Promise<FieldValues>;
86
87
  export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
87
- export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[];
88
+ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
88
89
  /**
89
90
  * Converts a plain text string to RTF format suitable for a RichTextEditor.
90
91
  *
@@ -98,5 +99,6 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], obj
98
99
  * This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
99
100
  */
100
101
  export declare function plainTextToRtf(plainText: string): string;
101
- export declare function getFieldDefinition(entry: FormEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
102
+ export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
102
103
  export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
104
+ export declare function useFormById(formId: string, apiServices: ApiServices, errorMessage?: string): import("@tanstack/react-query/build/legacy/types").UseQueryResult<EvokeForm, Error>;
@@ -1,4 +1,5 @@
1
1
  import { LocalDateTime } from '@js-joda/core';
2
+ import { useQuery } from '@tanstack/react-query';
2
3
  import { jsonLogic } from 'json-logic-js-graphql';
3
4
  import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } from 'lodash';
4
5
  import { DateTime } from 'luxon';
@@ -350,31 +351,19 @@ export function updateCriteriaInputs(criteria, data, user, instance) {
350
351
  },
351
352
  }));
352
353
  }
353
- export async function fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instanceId, fetchedOptions, initialMiddleObjectInstances) {
354
- try {
355
- if ((fetchedOptions && !fetchedOptions[`${fieldDefinition.id}MiddleObject`]) || !fetchedOptions) {
356
- const fetchedMiddleObject = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), {
357
- params: {
358
- filter: { fields: ['properties', 'actions', 'rootObjectId'] },
359
- },
360
- });
361
- setFetchedOptions({ [`${fieldDefinition.id}MiddleObject`]: fetchedMiddleObject });
362
- }
363
- // Fetch the initial middle object instances
364
- const filter = instanceId ? getMiddleObjectFilter(fieldDefinition, instanceId) : {};
365
- if (!isArray(initialMiddleObjectInstances)) {
366
- const fetchedInitialMiddleObjectInstances = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
367
- params: { filter: JSON.stringify(filter) },
368
- });
369
- setFetchedOptions({
370
- [`${fieldDefinition.id}InitialMiddleObjectInstances`]: fetchedInitialMiddleObjectInstances,
371
- });
372
- }
373
- }
374
- catch (error) {
375
- console.error('Error fetching collection data:', error);
376
- }
377
- }
354
+ export const fetchMiddleObject = async (fieldDefinition, apiServices) => {
355
+ return await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), {
356
+ params: {
357
+ filter: { fields: ['properties', 'actions', 'rootObjectId'] },
358
+ },
359
+ });
360
+ };
361
+ export const fetchInitialMiddleObjectInstances = async (apiServices, fieldDefinition, instanceId) => {
362
+ const filter = getMiddleObjectFilter(fieldDefinition, instanceId);
363
+ return await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
364
+ params: { filter: JSON.stringify(filter) },
365
+ });
366
+ };
378
367
  export const getErrorCountForSection = (section, errors) => {
379
368
  const entries = section.entries || [];
380
369
  return isArray(section.entries)
@@ -958,3 +947,14 @@ function applyMaskToObfuscatedValue(value, mask) {
958
947
  }
959
948
  return maskedValue;
960
949
  }
950
+ export function useFormById(formId, apiServices, errorMessage) {
951
+ return useQuery({
952
+ queryKey: ['form', formId],
953
+ enabled: formId !== '_auto_' && !!formId,
954
+ staleTime: Infinity,
955
+ queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}`)),
956
+ meta: {
957
+ errorMessage,
958
+ },
959
+ });
960
+ }
@@ -1,3 +1,4 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
1
2
  import { render as baseRender, screen, waitFor, within } from '@testing-library/react';
2
3
  import userEvent from '@testing-library/user-event';
3
4
  import { isEmpty, isEqual, set } from 'lodash';
@@ -14,12 +15,9 @@ global.ResizeObserver = class ResizeObserver {
14
15
  unobserve() { }
15
16
  disconnect() { }
16
17
  };
17
- const WithProviders = ({ children }) => {
18
- return React.createElement(MemoryRouter, null, children);
19
- };
20
- const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
21
18
  describe('FormRenderer', () => {
22
19
  let server;
20
+ let queryClient;
23
21
  beforeAll(() => {
24
22
  server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
25
23
  const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
@@ -65,12 +63,28 @@ describe('FormRenderer', () => {
65
63
  }), http.get('/api/accessManagement/users', () => HttpResponse.json(users)));
66
64
  server.listen();
67
65
  });
68
- afterAll(() => {
69
- server.close();
66
+ beforeEach(() => {
67
+ // Create a fresh QueryClient for each test, need to pass `retry: false` to avoid retries interfering with error state tests
68
+ queryClient = new QueryClient({
69
+ defaultOptions: {
70
+ queries: {
71
+ retry: false,
72
+ },
73
+ },
74
+ });
70
75
  });
71
76
  afterEach(() => {
72
77
  server.resetHandlers();
78
+ queryClient.clear();
73
79
  });
80
+ afterEach(() => {
81
+ server.resetHandlers();
82
+ });
83
+ const WithProviders = ({ children }) => {
84
+ return (React.createElement(QueryClientProvider, { client: queryClient },
85
+ React.createElement(MemoryRouter, null, children)));
86
+ };
87
+ const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
74
88
  describe('validation criteria', () => {
75
89
  it(`filters related object field with validation criteria that references a related object's nested data`, async () => {
76
90
  const user = userEvent.setup();
@@ -414,7 +428,7 @@ describe('FormRenderer', () => {
414
428
  type: 'input',
415
429
  parameterId: 'specialtyType',
416
430
  display: {
417
- label: 'Speciality Type',
431
+ label: 'Specialty Type',
418
432
  mode: 'existingOnly',
419
433
  },
420
434
  },
@@ -500,7 +514,7 @@ describe('FormRenderer', () => {
500
514
  type: 'input',
501
515
  parameterId: 'specialtyType',
502
516
  display: {
503
- label: 'Speciality Type',
517
+ label: 'Specialty Type',
504
518
  },
505
519
  },
506
520
  ],
@@ -586,7 +600,7 @@ describe('FormRenderer', () => {
586
600
  type: 'input',
587
601
  parameterId: 'specialtyType',
588
602
  display: {
589
- label: 'Speciality Type',
603
+ label: 'Specialty Type',
590
604
  relatedObjectDisplay: 'dialogBox',
591
605
  mode: 'existingOnly',
592
606
  },
@@ -654,7 +668,7 @@ describe('FormRenderer', () => {
654
668
  type: 'input',
655
669
  parameterId: 'specialtyType',
656
670
  display: {
657
- label: 'Speciality Type',
671
+ label: 'Specialty Type',
658
672
  relatedObjectDisplay: 'dialogBox',
659
673
  mode: 'existingOnly',
660
674
  },
@@ -684,7 +698,7 @@ describe('FormRenderer', () => {
684
698
  type: 'input',
685
699
  parameterId: 'specialtyType',
686
700
  display: {
687
- label: 'Speciality Type',
701
+ label: 'Specialty Type',
688
702
  createActionId: '_create',
689
703
  },
690
704
  },
@@ -944,7 +958,7 @@ describe('FormRenderer', () => {
944
958
  type: 'input',
945
959
  parameterId: 'specialtyType',
946
960
  display: {
947
- label: 'Speciality Type',
961
+ label: 'Specialty Type',
948
962
  relatedObjectDisplay: 'dropdown',
949
963
  mode: 'existingOnly',
950
964
  },
@@ -1021,7 +1035,7 @@ describe('FormRenderer', () => {
1021
1035
  const user = userEvent.setup();
1022
1036
  render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
1023
1037
  // Navigate to and open dropdown
1024
- const dropdown = await screen.findByRole('combobox', { name: 'Speciality Type' });
1038
+ const dropdown = await screen.findByRole('combobox', { name: 'Specialty Type' });
1025
1039
  await user.click(dropdown);
1026
1040
  await screen.findByRole('listbox');
1027
1041
  // Verify that the existing option is present
@@ -1176,7 +1190,7 @@ describe('FormRenderer', () => {
1176
1190
  type: 'input',
1177
1191
  parameterId: 'specialtyType',
1178
1192
  display: {
1179
- label: 'Speciality Type',
1193
+ label: 'Specialty Type',
1180
1194
  createActionId: '_create',
1181
1195
  relatedObjectId: 'specialtyType',
1182
1196
  },
@@ -1497,7 +1511,26 @@ describe('FormRenderer', () => {
1497
1511
  id: 'testInstanceId',
1498
1512
  name: 'Test Instance',
1499
1513
  },
1500
- })));
1514
+ })),
1515
+ // This is called by refetchRelatedInstances() after successful POST
1516
+ http.get('/api/data/objects/collectionObject/instances', (req) => {
1517
+ const filter = new URL(req.request.url).searchParams.get('filter');
1518
+ if (filter) {
1519
+ // Return the newly created item when refetching
1520
+ return HttpResponse.json([
1521
+ {
1522
+ id: 'newCollectionItemId',
1523
+ name: 'New Collection Item',
1524
+ objectId: 'collectionObject',
1525
+ relatedObject: {
1526
+ id: 'testInstanceId',
1527
+ name: 'Test Instance',
1528
+ },
1529
+ },
1530
+ ]);
1531
+ }
1532
+ return HttpResponse.json([]);
1533
+ }));
1501
1534
  render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
1502
1535
  const addButton = await screen.findByRole('button', { name: /add/i });
1503
1536
  await user.click(addButton);
@@ -1,3 +1,4 @@
1
+ import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
1
2
  import { render as baseRender, screen, waitFor, within } from '@testing-library/react';
2
3
  import userEvent from '@testing-library/user-event';
3
4
  import { isEqual } from 'lodash';
@@ -14,12 +15,9 @@ global.ResizeObserver = class ResizeObserver {
14
15
  unobserve() { }
15
16
  disconnect() { }
16
17
  };
17
- const WithProviders = ({ children }) => {
18
- return React.createElement(MemoryRouter, null, children);
19
- };
20
- const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
21
18
  describe('FormRendererContainer', () => {
22
19
  let server;
20
+ let queryClient;
23
21
  beforeAll(() => {
24
22
  server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
25
23
  const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
@@ -45,7 +43,7 @@ describe('FormRendererContainer', () => {
45
43
  // The two objects in the array of conditions in the "where" filter represent the potential filters that can be applied when retrieving "specialty" instances.
46
44
  // The first object is for the the validation criteria, but it is empty if the "license" field, which is referenced in the validation criteria, hasn't been filled out yet.
47
45
  // The second object is for the search criteria which the user enters in the "specialty" field, but it is empty if no search text has been entered.
48
- if (isEqual(whereFilter, { and: [{}, {}] }))
46
+ if (isEqual(whereFilter, {}))
49
47
  return HttpResponse.json([
50
48
  rnSpecialtyType1,
51
49
  rnSpecialtyType2,
@@ -61,12 +59,35 @@ describe('FormRendererContainer', () => {
61
59
  }));
62
60
  server.listen();
63
61
  });
62
+ beforeEach(() => {
63
+ // Create a fresh QueryClient for each test, need to pass `retry: false` to avoid retries interfering with error state tests
64
+ queryClient = new QueryClient({
65
+ queryCache: new QueryCache({
66
+ onError: (error, query) => {
67
+ const message = query.meta?.errorMessage ?? 'Something went wrong:';
68
+ console.error(message, error);
69
+ },
70
+ }),
71
+ defaultOptions: {
72
+ queries: {
73
+ retry: false,
74
+ },
75
+ },
76
+ });
77
+ });
64
78
  afterAll(() => {
65
79
  server.close();
80
+ queryClient.clear();
66
81
  });
67
82
  afterEach(() => {
68
83
  server.resetHandlers();
84
+ queryClient.clear();
69
85
  });
86
+ const WithProviders = ({ children }) => {
87
+ return (React.createElement(QueryClientProvider, { client: queryClient },
88
+ React.createElement(MemoryRouter, null, children)));
89
+ };
90
+ const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
70
91
  describe('validation criteria', () => {
71
92
  it(`filters related object field with validation criteria that references a defaulted related object's nested data`, async () => {
72
93
  const user = userEvent.setup();
@@ -123,9 +144,6 @@ describe('FormRendererContainer', () => {
123
144
  });
124
145
  }));
125
146
  render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
126
- await waitFor(() => {
127
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
128
- });
129
147
  const nameField = await screen.findByRole('textbox', { name: 'Name' });
130
148
  // Clear the existing value and type new value
131
149
  await user.clear(nameField);
@@ -173,9 +191,6 @@ describe('FormRendererContainer', () => {
173
191
  });
174
192
  }));
175
193
  render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
176
- await waitFor(() => {
177
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
178
- });
179
194
  const nameField = await screen.findByRole('textbox', { name: 'Name' });
180
195
  await user.type(nameField, 'Test Specialty');
181
196
  await user.tab();
@@ -220,9 +235,6 @@ describe('FormRendererContainer', () => {
220
235
  return HttpResponse.json({ error: 'Save failed' }, { status: 500 });
221
236
  }));
222
237
  render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
223
- await waitFor(() => {
224
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
225
- });
226
238
  const nameField = await screen.findByRole('textbox', { name: 'Name' });
227
239
  await user.type(nameField, 'Test Specialty');
228
240
  await user.tab();
@@ -264,9 +276,6 @@ describe('FormRendererContainer', () => {
264
276
  });
265
277
  }));
266
278
  render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
267
- await waitFor(() => {
268
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
269
- });
270
279
  const nameField = await screen.findByRole('textbox', { name: 'Name' });
271
280
  // Click into the field and blur it without changing value
272
281
  await user.click(nameField);
@@ -317,9 +326,6 @@ describe('FormRendererContainer', () => {
317
326
  });
318
327
  }));
319
328
  render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
320
- await waitFor(() => {
321
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
322
- });
323
329
  // Find the city field
324
330
  const cityField = await screen.findByRole('textbox', { name: 'City' });
325
331
  // Clear and type new value
@@ -372,9 +378,6 @@ describe('FormRendererContainer', () => {
372
378
  });
373
379
  }));
374
380
  render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
375
- await waitFor(() => {
376
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
377
- });
378
381
  // Find the city field
379
382
  const cityField = await screen.findByRole('textbox', { name: 'City' });
380
383
  // Click into field and blur without changing
@@ -433,9 +436,6 @@ describe('FormRendererContainer', () => {
433
436
  });
434
437
  }));
435
438
  render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
436
- await waitFor(() => {
437
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
438
- });
439
439
  // Find the line1 field (it's a searchbox because of address autocomplete)
440
440
  const line1Field = await screen.findByRole('searchbox', { name: 'Address Line 1' });
441
441
  // Type to trigger autocomplete
@@ -472,9 +472,7 @@ describe('FormRendererContainer', () => {
472
472
  });
473
473
  }));
474
474
  render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
475
- await waitFor(() => {
476
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
477
- });
475
+ await screen.findByRole('button', { name: 'Submit' });
478
476
  // When autosaveActionId is present the discard button should be hidden
479
477
  expect(screen.queryByRole('button', { name: /discard/i })).not.toBeInTheDocument();
480
478
  });
@@ -505,11 +503,7 @@ describe('FormRendererContainer', () => {
505
503
  }));
506
504
  const user = userEvent.setup();
507
505
  render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
508
- await waitFor(() => {
509
- expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
510
- });
511
- // Change a field value
512
- const nameInput = screen.getByRole('textbox', { name: /name/i });
506
+ const nameInput = await screen.findByRole('textbox', { name: /name/i });
513
507
  await user.clear(nameInput);
514
508
  await user.type(nameInput, 'Test Specialty');
515
509
  await user.tab();
@@ -1020,7 +1014,7 @@ describe('FormRendererContainer', () => {
1020
1014
  properties: [],
1021
1015
  };
1022
1016
  server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
1023
- render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
1017
+ render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, actionId: '_create' }));
1024
1018
  await screen.findByRole('button', { name: 'Submit' });
1025
1019
  });
1026
1020
  it('should display a button to discard changes', async () => {
@@ -1046,7 +1040,7 @@ describe('FormRendererContainer', () => {
1046
1040
  properties: [],
1047
1041
  };
1048
1042
  server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
1049
- render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
1043
+ render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, actionId: '_create' }));
1050
1044
  await screen.findByRole('button', { name: 'Discard Changes' });
1051
1045
  });
1052
1046
  it('should reset the form when discarding changes', async () => {
@@ -1089,7 +1083,7 @@ describe('FormRendererContainer', () => {
1089
1083
  };
1090
1084
  server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
1091
1085
  const user = userEvent.setup();
1092
- render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
1086
+ render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, actionId: '_create' }));
1093
1087
  const firstNameInput = await screen.findByRole('textbox', { name: 'First Name' });
1094
1088
  await user.type(firstNameInput, 'John');
1095
1089
  const discardButton = await screen.findByRole('button', { name: 'Discard Changes' });
@@ -1143,10 +1137,8 @@ describe('FormRendererContainer', () => {
1143
1137
  };
1144
1138
  server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)), http.get('/api/data/objects/simpleObject/instances/123', () => HttpResponse.json({
1145
1139
  message: 'Not Found',
1146
- }, { status: 404 })), http.get('/api/data/objects/simpleObject/instances/123/object', () => HttpResponse.json({
1147
- message: 'Not Found',
1148
- }, { status: 404 })));
1149
- render(React.createElement(FormRendererContainer, { formId: form.id, actionId: "_update", objectId: "simpleObject", instanceId: '123', dataType: "objectInstances" }));
1140
+ }, { status: 404 })), http.get('/api/data/objects/simpleObject/instances/123/object', () => HttpResponse.json(simpleObject)));
1141
+ render(React.createElement(FormRendererContainer, { formId: form.id, actionId: "_update", objectId: "simpleObject", instanceId: '123' }));
1150
1142
  await screen.findByText('The requested content could not be found.');
1151
1143
  });
1152
1144
  it('should show an unauthorized error if the instance access is unauthorized', async () => {
@@ -1243,7 +1235,7 @@ describe('FormRendererContainer', () => {
1243
1235
  properties: [],
1244
1236
  };
1245
1237
  server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)));
1246
- render(React.createElement(FormRendererContainer, { formId: form.id, objectId: "simpleObject", dataType: "objectInstances" }));
1238
+ render(React.createElement(FormRendererContainer, { formId: form.id, objectId: "simpleObject", actionId: "_create" }));
1247
1239
  await screen.findByText('Simple Form');
1248
1240
  });
1249
1241
  it('should show a not found error when the form cannot be found', async () => {
@@ -1474,7 +1466,7 @@ describe('FormRendererContainer', () => {
1474
1466
  });
1475
1467
  it('should display validation errors after trying to submit the form', async () => {
1476
1468
  const user = userEvent.setup();
1477
- render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
1469
+ render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
1478
1470
  const submitButton = await screen.findByRole('button', { name: 'Submit' });
1479
1471
  await user.click(submitButton);
1480
1472
  // List items are named by author, but they don't
@@ -1485,7 +1477,7 @@ describe('FormRendererContainer', () => {
1485
1477
  });
1486
1478
  it('should clear validation errors after they have been resolved', async () => {
1487
1479
  const user = userEvent.setup();
1488
- render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
1480
+ render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
1489
1481
  const submitButton = await screen.findByRole('button', { name: 'Submit' });
1490
1482
  await user.click(submitButton);
1491
1483
  // Make sure error elements appear
@@ -1496,7 +1488,7 @@ describe('FormRendererContainer', () => {
1496
1488
  });
1497
1489
  it('should scroll to validation errors after submission', async () => {
1498
1490
  const user = userEvent.setup();
1499
- render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
1491
+ render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
1500
1492
  const submitButton = await screen.findByRole('button', { name: 'Submit' });
1501
1493
  await user.click(submitButton);
1502
1494
  expect(scrollIntoViewMock).toHaveBeenCalled();
@@ -1504,7 +1496,7 @@ describe('FormRendererContainer', () => {
1504
1496
  it('should not scroll to validation errors after submission if there are none', async () => {
1505
1497
  const user = userEvent.setup();
1506
1498
  server.use(http.post(`/api/data/objects/${validationTestObject.id}/instances/actions`, () => HttpResponse.json({}, { status: 200 })));
1507
- render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
1499
+ render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
1508
1500
  const requiredField = await screen.findByRole('textbox', { name: /Required Field */i });
1509
1501
  await user.type(requiredField, 'Some content here...');
1510
1502
  const submitButton = await screen.findByRole('button', { name: 'Submit' });
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import { EntryRendererProps } from '../FormV2/components/types';
2
- declare function ViewOnlyEntryRenderer(props: EntryRendererProps): any;
3
+ declare function ViewOnlyEntryRenderer(props: EntryRendererProps): React.JSX.Element | null;
3
4
  export default ViewOnlyEntryRenderer;
@@ -1,11 +1,12 @@
1
1
  import { useApiServices, useApp, } from '@evoke-platform/context';
2
2
  import { CancelRounded, CheckCircleRounded } from '@mui/icons-material';
3
+ import { useQuery } from '@tanstack/react-query';
3
4
  import DOMPurify from 'dompurify';
4
5
  import { isEmpty, isNil } from 'lodash';
5
6
  import { DateTime } from 'luxon';
6
7
  import React, { useEffect, useMemo, useState } from 'react';
7
8
  import { useFormContext } from '../../../theme/hooks';
8
- import { Link, Typography } from '../../core';
9
+ import { Link, Skeleton, Typography } from '../../core';
9
10
  import { Box, Grid } from '../../layout';
10
11
  import AccordionSections from '../FormV2/components/AccordionSections';
11
12
  import FieldWrapper from '../FormV2/components/FieldWrapper';
@@ -16,7 +17,7 @@ import Criteria from '../FormV2/components/FormFieldTypes/Criteria';
16
17
  import { Document } from '../FormV2/components/FormFieldTypes/DocumentFiles/Document';
17
18
  import { Image } from '../FormV2/components/FormFieldTypes/Image';
18
19
  import PropertyProtection from '../FormV2/components/PropertyProtection';
19
- import { entryIsVisible, fetchCollectionData, filterEmptySections, getDefaultPages, isAddressProperty, } from '../FormV2/components/utils';
20
+ import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getDefaultPages, isAddressProperty, } from '../FormV2/components/utils';
20
21
  function ViewOnlyEntryRenderer(props) {
21
22
  const { entry } = props;
22
23
  const { fetchedOptions, setFetchedOptions, object, instance, richTextEditor: RichTextEditor } = useFormContext();
@@ -26,8 +27,6 @@ function ViewOnlyEntryRenderer(props) {
26
27
  const [navigationSlug, setNavigationSlug] = useState(fetchedOptions[`${entryId}NavigationSlug`]);
27
28
  const [currentDisplayValue, setCurrentDisplayValue] = useState(instance?.[entryId]);
28
29
  const [protectionMode, setProtectionMode] = useState('mask');
29
- const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
30
- const middleObject = fetchedOptions[`${entryId}MiddleObject`];
31
30
  const display = 'display' in entry ? entry.display : undefined;
32
31
  const fieldDefinition = useMemo(() => {
33
32
  const def = entry.type === 'readonlyField'
@@ -45,11 +44,35 @@ function ViewOnlyEntryRenderer(props) {
45
44
  }, [entry, object]);
46
45
  const isProtectedProperty = fieldDefinition?.protection?.maskChar;
47
46
  const protectionComponent = isProtectedProperty && !isNil(currentDisplayValue) ? (React.createElement(PropertyProtection, { parameter: fieldDefinition, protection: fieldDefinition?.protection, mask: fieldDefinition?.mask, value: currentDisplayValue, canEdit: false, setCurrentDisplayValue: setCurrentDisplayValue, mode: protectionMode, setMode: setProtectionMode })) : null;
48
- useEffect(() => {
49
- if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
50
- fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
51
- }
52
- }, [fieldDefinition, initialMiddleObjectInstances, setFetchedOptions, instance, fetchedOptions, apiServices]);
47
+ const { data: middleObject } = useQuery({
48
+ queryKey: [fieldDefinition?.objectId, 'MiddleObject'],
49
+ queryFn: () => fetchMiddleObject(fieldDefinition, apiServices),
50
+ staleTime: Infinity,
51
+ enabled: !!(fieldDefinition?.objectId &&
52
+ fieldDefinition?.type === 'collection' &&
53
+ fieldDefinition?.manyToManyPropertyId),
54
+ meta: {
55
+ errorMessage: 'Failed to fetch middle object: ',
56
+ },
57
+ });
58
+ const { data: initialMiddleObjectInstances = [], isLoading: isLoadingInstances } = useQuery({
59
+ queryKey: [
60
+ fieldDefinition?.objectId,
61
+ instance?.id,
62
+ fieldDefinition?.relatedPropertyId,
63
+ 'InitialMiddleObjectInstances',
64
+ ],
65
+ queryFn: () => fetchInitialMiddleObjectInstances(apiServices, fieldDefinition, instance?.id),
66
+ staleTime: Infinity,
67
+ enabled: !!(fieldDefinition?.objectId &&
68
+ instance?.id &&
69
+ fieldDefinition?.type === 'collection' &&
70
+ fieldDefinition?.manyToManyPropertyId &&
71
+ fieldDefinition?.relatedPropertyId),
72
+ meta: {
73
+ errorMessage: 'Failed to fetch middle object instances: ',
74
+ },
75
+ });
53
76
  useEffect(() => {
54
77
  (async () => {
55
78
  if (object?.properties && !fetchedOptions[`${entryId}NavigationSlug`]) {
@@ -146,9 +169,11 @@ function ViewOnlyEntryRenderer(props) {
146
169
  React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: false, value: fieldValue, canUpdateProperty: false })));
147
170
  }
148
171
  else if (fieldDefinition.type === 'collection') {
149
- return fieldDefinition?.manyToManyPropertyId ? (middleObject && initialMiddleObjectInstances && (React.createElement(FieldWrapper, { inputId: entryId, inputType: 'collection', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
150
- React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] ||
151
- initialMiddleObjectInstances, id: entryId, middleObject: middleObject, fieldDefinition: fieldDefinition, readOnly: true })))) : (React.createElement(FieldWrapper, { inputId: entryId, inputType: 'collection', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
172
+ if (fieldDefinition.manyToManyPropertyId && !isEmpty(middleObject)) {
173
+ return isLoadingInstances ? (React.createElement(Skeleton, null)) : (React.createElement(FieldWrapper, { inputId: entryId, inputType: 'collection', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
174
+ React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: initialMiddleObjectInstances, id: entryId, middleObject: middleObject, fieldDefinition: fieldDefinition, readOnly: true })));
175
+ }
176
+ return (React.createElement(FieldWrapper, { inputId: entryId, inputType: 'collection', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
152
177
  React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: false, entry: entry, viewLayout: display?.viewLayout })));
153
178
  }
154
179
  else if (fieldDefinition.type === 'criteria') {
@@ -6,10 +6,15 @@ import { Skeleton, Snackbar } from '../../core';
6
6
  import { Box } from '../../layout';
7
7
  import ErrorComponent from '../ErrorComponent';
8
8
  import { FormContext } from '../FormV2';
9
+ import ConditionalQueryClientProvider from '../FormV2/components/ConditionalQueryClientProvider';
9
10
  import Header from '../FormV2/components/Header';
10
11
  import { assignIdsToSectionsAndRichText, getPrefixedUrl } from '../FormV2/components/utils';
11
12
  import ViewOnlyEntryRenderer from './InstanceEntryRenderer';
12
13
  function ViewDetailsV2Container(props) {
14
+ return (React.createElement(ConditionalQueryClientProvider, null,
15
+ React.createElement(ViewDetailsV2ContainerInner, { ...props })));
16
+ }
17
+ function ViewDetailsV2ContainerInner(props) {
13
18
  const { instanceId, panelLayoutId, objectId, richTextEditor, renderHeader, renderBody } = props;
14
19
  const apiServices = useApiServices();
15
20
  const [sanitizedObject, setSanitizedObject] = useState();
@@ -67,8 +72,8 @@ function ViewDetailsV2Container(props) {
67
72
  if (panelLayoutId || sanitizedObject?.defaultPanelLayoutId) {
68
73
  apiServices
69
74
  .get(getPrefixedUrl(`/objects/${objectId}/panelLayouts/${panelLayoutId || sanitizedObject?.defaultPanelLayoutId}`))
70
- .then((evokeForm) => {
71
- setPanelLayout(evokeForm);
75
+ .then((panel) => {
76
+ setPanelLayout(panel);
72
77
  })
73
78
  .catch((error) => {
74
79
  onError(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.13.0-dev.6",
3
+ "version": "1.13.0-dev.7",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",
@@ -96,7 +96,7 @@
96
96
  "yalc": "^1.0.0-pre.53"
97
97
  },
98
98
  "peerDependencies": {
99
- "@evoke-platform/context": "^1.7.0",
99
+ "@evoke-platform/context": "1.8.0-dev.4",
100
100
  "react": "^18.1.0",
101
101
  "react-dom": "^18.1.0"
102
102
  },
@@ -118,6 +118,7 @@
118
118
  "@mui/x-tree-view": "^7.29.1",
119
119
  "@react-querybuilder/dnd": "^5.4.1",
120
120
  "@react-querybuilder/material": "^6.5.0",
121
+ "@tanstack/react-query": "^5.90.12",
121
122
  "clean-deep": "^3.4.0",
122
123
  "commit-and-tag-version": "^12.4.1",
123
124
  "devexpress-richedit": "^23.1.5",