@evoke-platform/ui-components 1.13.0 → 1.15.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 (40) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +2 -2
  2. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +0 -15
  3. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +0 -10
  4. package/dist/published/components/custom/CriteriaBuilder/utils.js +2 -161
  5. package/dist/published/components/custom/Form/utils.js +3 -2
  6. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
  7. package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
  8. package/dist/published/components/custom/FormV2/FormRendererContainer.js +132 -101
  9. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
  10. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +105 -185
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +18 -26
  17. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +18 -18
  18. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +96 -169
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +57 -13
  22. package/dist/published/components/custom/FormV2/components/HtmlView.js +5 -1
  23. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
  24. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +61 -29
  25. package/dist/published/components/custom/FormV2/components/utils.d.ts +23 -4
  26. package/dist/published/components/custom/FormV2/components/utils.js +136 -26
  27. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +28 -14
  28. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +41 -48
  29. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
  30. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +56 -19
  31. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
  32. package/dist/published/components/custom/index.d.ts +2 -0
  33. package/dist/published/components/custom/index.js +1 -0
  34. package/dist/published/components/custom/types.d.ts +15 -0
  35. package/dist/published/components/custom/types.js +1 -0
  36. package/dist/published/components/custom/util.d.ts +10 -0
  37. package/dist/published/components/custom/util.js +161 -1
  38. package/dist/published/index.d.ts +2 -2
  39. package/dist/published/index.js +1 -1
  40. package/package.json +3 -4
@@ -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,17 +59,42 @@ 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();
73
94
  server.use(http.get('/api/data/objects/license/instances/rnLicense', () => {
74
95
  return HttpResponse.json(rnLicense);
96
+ }), http.get('/api/data/objects/specialtyType/instances', () => {
97
+ return HttpResponse.json([rnSpecialtyType1, rnSpecialtyType2]);
75
98
  }), http.get('/api/data/forms/specialtyForm', () => {
76
99
  return HttpResponse.json(createSpecialtyForm);
77
100
  }));
@@ -123,9 +146,6 @@ describe('FormRendererContainer', () => {
123
146
  });
124
147
  }));
125
148
  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
149
  const nameField = await screen.findByRole('textbox', { name: 'Name' });
130
150
  // Clear the existing value and type new value
131
151
  await user.clear(nameField);
@@ -173,9 +193,6 @@ describe('FormRendererContainer', () => {
173
193
  });
174
194
  }));
175
195
  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
196
  const nameField = await screen.findByRole('textbox', { name: 'Name' });
180
197
  await user.type(nameField, 'Test Specialty');
181
198
  await user.tab();
@@ -220,9 +237,6 @@ describe('FormRendererContainer', () => {
220
237
  return HttpResponse.json({ error: 'Save failed' }, { status: 500 });
221
238
  }));
222
239
  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
240
  const nameField = await screen.findByRole('textbox', { name: 'Name' });
227
241
  await user.type(nameField, 'Test Specialty');
228
242
  await user.tab();
@@ -264,9 +278,6 @@ describe('FormRendererContainer', () => {
264
278
  });
265
279
  }));
266
280
  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
281
  const nameField = await screen.findByRole('textbox', { name: 'Name' });
271
282
  // Click into the field and blur it without changing value
272
283
  await user.click(nameField);
@@ -317,9 +328,6 @@ describe('FormRendererContainer', () => {
317
328
  });
318
329
  }));
319
330
  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
331
  // Find the city field
324
332
  const cityField = await screen.findByRole('textbox', { name: 'City' });
325
333
  // Clear and type new value
@@ -372,9 +380,6 @@ describe('FormRendererContainer', () => {
372
380
  });
373
381
  }));
374
382
  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
383
  // Find the city field
379
384
  const cityField = await screen.findByRole('textbox', { name: 'City' });
380
385
  // Click into field and blur without changing
@@ -433,9 +438,6 @@ describe('FormRendererContainer', () => {
433
438
  });
434
439
  }));
435
440
  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
441
  // Find the line1 field (it's a searchbox because of address autocomplete)
440
442
  const line1Field = await screen.findByRole('searchbox', { name: 'Address Line 1' });
441
443
  // Type to trigger autocomplete
@@ -472,9 +474,7 @@ describe('FormRendererContainer', () => {
472
474
  });
473
475
  }));
474
476
  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
- });
477
+ await screen.findByRole('button', { name: 'Submit' });
478
478
  // When autosaveActionId is present the discard button should be hidden
479
479
  expect(screen.queryByRole('button', { name: /discard/i })).not.toBeInTheDocument();
480
480
  });
@@ -505,11 +505,7 @@ describe('FormRendererContainer', () => {
505
505
  }));
506
506
  const user = userEvent.setup();
507
507
  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 });
508
+ const nameInput = await screen.findByRole('textbox', { name: /name/i });
513
509
  await user.clear(nameInput);
514
510
  await user.type(nameInput, 'Test Specialty');
515
511
  await user.tab();
@@ -1104,7 +1100,7 @@ describe('FormRendererContainer', () => {
1104
1100
  properties: [],
1105
1101
  };
1106
1102
  server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
1107
- render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
1103
+ render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, actionId: '_create' }));
1108
1104
  await screen.findByRole('button', { name: 'Submit' });
1109
1105
  });
1110
1106
  it('should display a button to discard changes', async () => {
@@ -1130,7 +1126,7 @@ describe('FormRendererContainer', () => {
1130
1126
  properties: [],
1131
1127
  };
1132
1128
  server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
1133
- render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
1129
+ render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, actionId: '_create' }));
1134
1130
  await screen.findByRole('button', { name: 'Discard Changes' });
1135
1131
  });
1136
1132
  it('should reset the form when discarding changes', async () => {
@@ -1173,7 +1169,7 @@ describe('FormRendererContainer', () => {
1173
1169
  };
1174
1170
  server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
1175
1171
  const user = userEvent.setup();
1176
- render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
1172
+ render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, actionId: '_create' }));
1177
1173
  const firstNameInput = await screen.findByRole('textbox', { name: 'First Name' });
1178
1174
  await user.type(firstNameInput, 'John');
1179
1175
  const discardButton = await screen.findByRole('button', { name: 'Discard Changes' });
@@ -1227,10 +1223,8 @@ describe('FormRendererContainer', () => {
1227
1223
  };
1228
1224
  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({
1229
1225
  message: 'Not Found',
1230
- }, { status: 404 })), http.get('/api/data/objects/simpleObject/instances/123/object', () => HttpResponse.json({
1231
- message: 'Not Found',
1232
- }, { status: 404 })));
1233
- render(React.createElement(FormRendererContainer, { formId: form.id, actionId: "_update", objectId: "simpleObject", instanceId: '123', dataType: "objectInstances" }));
1226
+ }, { status: 404 })), http.get('/api/data/objects/simpleObject/instances/123/object', () => HttpResponse.json(simpleObject)));
1227
+ render(React.createElement(FormRendererContainer, { formId: form.id, actionId: "_update", objectId: "simpleObject", instanceId: '123' }));
1234
1228
  await screen.findByText('The requested content could not be found.');
1235
1229
  });
1236
1230
  it('should show an unauthorized error if the instance access is unauthorized', async () => {
@@ -1327,7 +1321,7 @@ describe('FormRendererContainer', () => {
1327
1321
  properties: [],
1328
1322
  };
1329
1323
  server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)));
1330
- render(React.createElement(FormRendererContainer, { formId: form.id, objectId: "simpleObject", dataType: "objectInstances" }));
1324
+ render(React.createElement(FormRendererContainer, { formId: form.id, objectId: "simpleObject", actionId: "_create" }));
1331
1325
  await screen.findByText('Simple Form');
1332
1326
  });
1333
1327
  it('should show a not found error when the form cannot be found', async () => {
@@ -1558,7 +1552,7 @@ describe('FormRendererContainer', () => {
1558
1552
  });
1559
1553
  it('should display validation errors after trying to submit the form', async () => {
1560
1554
  const user = userEvent.setup();
1561
- render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
1555
+ render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
1562
1556
  const submitButton = await screen.findByRole('button', { name: 'Submit' });
1563
1557
  await user.click(submitButton);
1564
1558
  // List items are named by author, but they don't
@@ -1569,7 +1563,7 @@ describe('FormRendererContainer', () => {
1569
1563
  });
1570
1564
  it('should clear validation errors after they have been resolved', async () => {
1571
1565
  const user = userEvent.setup();
1572
- render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
1566
+ render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
1573
1567
  const submitButton = await screen.findByRole('button', { name: 'Submit' });
1574
1568
  await user.click(submitButton);
1575
1569
  // Make sure error elements appear
@@ -1580,7 +1574,7 @@ describe('FormRendererContainer', () => {
1580
1574
  });
1581
1575
  it('should scroll to validation errors after submission', async () => {
1582
1576
  const user = userEvent.setup();
1583
- render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
1577
+ render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
1584
1578
  const submitButton = await screen.findByRole('button', { name: 'Submit' });
1585
1579
  await user.click(submitButton);
1586
1580
  expect(scrollIntoViewMock).toHaveBeenCalled();
@@ -1588,7 +1582,7 @@ describe('FormRendererContainer', () => {
1588
1582
  it('should not scroll to validation errors after submission if there are none', async () => {
1589
1583
  const user = userEvent.setup();
1590
1584
  server.use(http.post(`/api/data/objects/${validationTestObject.id}/instances/actions`, () => HttpResponse.json({}, { status: 200 })));
1591
- render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
1585
+ render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, actionId: "_create" }));
1592
1586
  const requiredField = await screen.findByRole('textbox', { name: /Required Field */i });
1593
1587
  await user.type(requiredField, 'Some content here...');
1594
1588
  const submitButton = await screen.findByRole('button', { name: 'Submit' });
@@ -1611,8 +1605,7 @@ describe('FormRendererContainer', () => {
1611
1605
  // Wait for the delete confirmation message to appear
1612
1606
  const confirmation = await screen.findByText(/you are about to delete/i);
1613
1607
  expect(confirmation).toBeInTheDocument();
1614
- // Validate that the message includes the instance name
1615
- expect(confirmation).toHaveTextContent(/Persons Name/);
1608
+ await screen.findByText(/Persons Name/);
1616
1609
  // Ensure the "Delete" button is rendered
1617
1610
  await screen.findByRole('button', { name: /delete/i });
1618
1611
  });
@@ -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,10 +1,11 @@
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 { isEmpty, isNil } from 'lodash';
4
5
  import { DateTime } from 'luxon';
5
6
  import React, { useEffect, useMemo, useState } from 'react';
6
7
  import { useFormContext } from '../../../theme/hooks';
7
- import { Link, Typography } from '../../core';
8
+ import { Link, Skeleton, Typography } from '../../core';
8
9
  import { Box, Grid } from '../../layout';
9
10
  import AccordionSections from '../FormV2/components/AccordionSections';
10
11
  import FieldWrapper from '../FormV2/components/FieldWrapper';
@@ -15,7 +16,7 @@ import Criteria from '../FormV2/components/FormFieldTypes/Criteria';
15
16
  import { Document } from '../FormV2/components/FormFieldTypes/DocumentFiles/Document';
16
17
  import { Image } from '../FormV2/components/FormFieldTypes/Image';
17
18
  import PropertyProtection from '../FormV2/components/PropertyProtection';
18
- import { entryIsVisible, fetchCollectionData, filterEmptySections, getDefaultPages, isAddressProperty, } from '../FormV2/components/utils';
19
+ import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getDefaultPages, isAddressProperty, } from '../FormV2/components/utils';
19
20
  import HtmlView from '../FormV2/components/HtmlView';
20
21
  function ViewOnlyEntryRenderer(props) {
21
22
  const { entry } = props;
@@ -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,24 +44,60 @@ 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`]) {
56
- const pages = await getDefaultPages(object.properties, defaultPages, findDefaultPageSlugFor);
57
- if (fieldDefinition?.objectId && pages[fieldDefinition.objectId]) {
58
- setNavigationSlug(pages[fieldDefinition.objectId]);
59
- setFetchedOptions({
60
- [`${entryId}NavigationSlug`]: pages[fieldDefinition.objectId],
61
- });
79
+ const property = object.properties.find((prop) => prop.id === entryId);
80
+ if (!property || property.type !== 'object') {
81
+ return;
82
+ }
83
+ if (!property.objectId) {
84
+ property.objectId = instance?.[entryId] && (instance?.[entryId]).objectId;
85
+ }
86
+ try {
87
+ const pages = await getDefaultPages([property], defaultPages, findDefaultPageSlugFor);
88
+ if (property?.objectId && pages[property.objectId]) {
89
+ setNavigationSlug(pages[property.objectId]);
90
+ setFetchedOptions({
91
+ [`${entryId}NavigationSlug`]: pages[property.objectId],
92
+ });
93
+ }
94
+ }
95
+ catch (error) {
96
+ console.error('Error fetching default pages:', error);
62
97
  }
63
98
  }
64
99
  })();
65
- }, [object, defaultPages, findDefaultPageSlugFor, fieldDefinition]);
100
+ }, [object, defaultPages, findDefaultPageSlugFor, instance, entryId, fetchedOptions]);
66
101
  // If the entry is hidden, clear its value and any nested values, and skip rendering
67
102
  if (!entryIsVisible(entry, instance)) {
68
103
  return null;
@@ -132,9 +167,11 @@ function ViewOnlyEntryRenderer(props) {
132
167
  React.createElement(Document, { id: entryId, error: false, value: fieldValue, canUpdateProperty: false })));
133
168
  }
134
169
  else if (fieldDefinition.type === 'collection') {
135
- 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 },
136
- React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] ||
137
- 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 },
170
+ if (fieldDefinition.manyToManyPropertyId && !isEmpty(middleObject)) {
171
+ 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 },
172
+ React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: initialMiddleObjectInstances, id: entryId, middleObject: middleObject, fieldDefinition: fieldDefinition, readOnly: true })));
173
+ }
174
+ return (React.createElement(FieldWrapper, { inputId: entryId, inputType: 'collection', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
138
175
  React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: false, entry: entry, viewLayout: display?.viewLayout })));
139
176
  }
140
177
  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, title, 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);
@@ -16,3 +16,5 @@ export { RichTextViewer } from './RichTextViewer';
16
16
  export { UserAvatar } from './UserAvatar';
17
17
  export { ViewDetailsV2Container, ViewOnlyEntryRenderer } from './ViewDetailsV2';
18
18
  export type { ViewDetailsV2ContainerProps } from './ViewDetailsV2';
19
+ export * from './util';
20
+ export type { MongoDBQueryValue } from './types';
@@ -13,3 +13,4 @@ export { ResponsiveOverflow } from './ResponsiveOverflow';
13
13
  export { RichTextViewer } from './RichTextViewer';
14
14
  export { UserAvatar } from './UserAvatar';
15
15
  export { ViewDetailsV2Container, ViewOnlyEntryRenderer } from './ViewDetailsV2';
16
+ export * from './util';
@@ -0,0 +1,15 @@
1
+ export type MongoDBQueryValue = null | string | boolean | {
2
+ $not?: {
3
+ $regex?: string;
4
+ };
5
+ $regex?: string;
6
+ $expr?: boolean;
7
+ $eq?: unknown;
8
+ $ne?: unknown;
9
+ $lt?: unknown;
10
+ $lte?: unknown;
11
+ $gt?: unknown;
12
+ $gte?: unknown;
13
+ $in?: unknown[];
14
+ $nin?: unknown[];
15
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1,11 @@
1
+ import { DefaultRuleGroupType } from 'react-querybuilder';
1
2
  export declare function difference(object?: any, base?: any): any;
3
+ /**
4
+ * Parses a MongoDB query into a DefaultRuleGroupType or a single RuleType.
5
+ * This function recursively processes the MongoDB query and transforms it into
6
+ * a structured format that can be used in the CriteriaBuilder.
7
+ *
8
+ * @param {Record<string, unknown>} mongoQuery - The MongoDB query to be parsed.
9
+ * @returns {DefaultRuleGroupType} - Correctly formatted rule or rules for the query builder.
10
+ */
11
+ export declare function parseMongoDB(mongoQuery: Record<string, unknown>): DefaultRuleGroupType;
@@ -1,4 +1,4 @@
1
- import { isEqual, isObject, transform } from 'lodash';
1
+ import { isArray, isEmpty, isEqual, isObject, transform } from 'lodash';
2
2
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
3
  export function difference(object, base) {
4
4
  if (!object) {
@@ -14,3 +14,163 @@ export function difference(object, base) {
14
14
  }
15
15
  });
16
16
  }
17
+ /**
18
+ * Parses a MongoDB query into a DefaultRuleGroupType or a single RuleType.
19
+ * This function recursively processes the MongoDB query and transforms it into
20
+ * a structured format that can be used in the CriteriaBuilder.
21
+ *
22
+ * @param {Record<string, unknown>} mongoQuery - The MongoDB query to be parsed.
23
+ * @returns {DefaultRuleGroupType} - Correctly formatted rule or rules for the query builder.
24
+ */
25
+ export function parseMongoDB(mongoQuery) {
26
+ /**
27
+ * Parses a single rule from the MongoDB query.
28
+ *
29
+ * @param {string} key - The field name for the rule.
30
+ * @param {MongoDBQueryValue} value - The value associated with the key to parse.
31
+ * @returns {DefaultRuleType | undefined} - A DefaultRuleType if the value is valid, otherwise undefined.
32
+ */
33
+ const parseRule = (key, value) => {
34
+ if (key === '$expr') {
35
+ return undefined;
36
+ }
37
+ else if (value === null) {
38
+ return {
39
+ field: key,
40
+ operator: 'null',
41
+ value: null,
42
+ };
43
+ }
44
+ else if (typeof value === 'string' ||
45
+ typeof value === 'number' ||
46
+ typeof value === 'boolean' ||
47
+ '$eq' in value) {
48
+ return {
49
+ field: key,
50
+ operator: '=',
51
+ value: typeof value === 'object' && '$eq' in value ? value.$eq : value,
52
+ };
53
+ }
54
+ else if (value.$not && typeof value.$not.$regex === 'string') {
55
+ return {
56
+ field: key,
57
+ operator: 'doesNotContain',
58
+ value: value.$not.$regex,
59
+ };
60
+ }
61
+ else if (typeof value.$regex === 'string') {
62
+ let operator = 'contains';
63
+ let regexValue = value.$regex;
64
+ if (regexValue.startsWith('^')) {
65
+ operator = 'beginsWith';
66
+ regexValue = regexValue.slice(1);
67
+ }
68
+ else if (regexValue.endsWith('$')) {
69
+ operator = 'endsWith';
70
+ regexValue = regexValue.slice(0, -1);
71
+ }
72
+ if (regexValue) {
73
+ // remove escape characters for display
74
+ regexValue = regexValue.replace(/\\(.)/g, '$1');
75
+ }
76
+ return {
77
+ field: key,
78
+ operator: operator,
79
+ value: regexValue,
80
+ };
81
+ }
82
+ else if ('$ne' in value) {
83
+ return {
84
+ field: key,
85
+ operator: value.$ne === null ? 'notNull' : '!=',
86
+ value: value.$ne,
87
+ };
88
+ }
89
+ else if ('$lt' in value) {
90
+ return {
91
+ field: key,
92
+ operator: '<',
93
+ value: value.$lt,
94
+ };
95
+ }
96
+ else if ('$lte' in value) {
97
+ return {
98
+ field: key,
99
+ operator: '<=',
100
+ value: value.$lte,
101
+ };
102
+ }
103
+ else if ('$gt' in value) {
104
+ return {
105
+ field: key,
106
+ operator: '>',
107
+ value: value.$gt,
108
+ };
109
+ }
110
+ else if ('$gte' in value) {
111
+ return {
112
+ field: key,
113
+ operator: '>=',
114
+ value: value.$gte,
115
+ };
116
+ }
117
+ else if ('$in' in value) {
118
+ return {
119
+ field: key,
120
+ operator: 'in',
121
+ value: value.$in ?? [],
122
+ };
123
+ }
124
+ else if ('$nin' in value) {
125
+ return {
126
+ field: key,
127
+ operator: 'notIn',
128
+ value: value.$nin ?? [],
129
+ };
130
+ }
131
+ else {
132
+ return undefined;
133
+ }
134
+ };
135
+ /**
136
+ * Recursively parses a MongoDB query into a RuleGroupType or RuleType.
137
+ *
138
+ * @param {Record<string, unknown>} query - The MongoDB query object to be parsed.
139
+ * @returns {DefaultRuleGroupType | DefaultRuleType} - A RuleGroupType with combinator and rules, or a single RuleType.
140
+ */
141
+ const parseGroup = (query) => {
142
+ if ('$and' in query && isArray(query.$and)) {
143
+ return {
144
+ combinator: 'and',
145
+ rules: query.$and.map(parseGroup).filter((rule) => rule !== undefined),
146
+ };
147
+ }
148
+ else if ('$or' in query && isArray(query.$or)) {
149
+ return {
150
+ combinator: 'or',
151
+ rules: query.$or.map(parseGroup).filter((rule) => rule !== undefined),
152
+ };
153
+ }
154
+ else if (isEmpty(query)) {
155
+ return { combinator: 'and', rules: [] };
156
+ }
157
+ else {
158
+ const rules = Object.entries(query)
159
+ .map(([key, value]) => parseRule(key, value))
160
+ .filter((rule) => rule !== null);
161
+ return rules[0];
162
+ }
163
+ };
164
+ const result = parseGroup(mongoQuery);
165
+ // Check if the result is a RuleGroupType (i.e., has a combinator)
166
+ if (result && 'combinator' in result) {
167
+ return result;
168
+ }
169
+ else {
170
+ // If there are no condition groups configured so it's not a RuleGroupType, wrap it in a default 'and' combinator
171
+ return {
172
+ combinator: 'and',
173
+ rules: result ? [result] : [],
174
+ };
175
+ }
176
+ }
@@ -6,8 +6,8 @@ export type { SxProps } from '@mui/system';
6
6
  export { CalendarPicker, DateTimePicker, MonthPicker, PickersDay, StaticDateTimePicker, StaticTimePicker, TimePicker, YearPicker, } from '@mui/x-date-pickers';
7
7
  export * from './colors';
8
8
  export * from './components/core';
9
- export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormContext, FormField, FormRenderer, FormRendererContainer, getReadableQuery, HistoryLog, MenuBar, MultiSelect, RecursiveEntryRenderer, RepeatableField, ResponsiveOverflow, RichTextViewer, UserAvatar, ViewDetailsV2Container, ViewOnlyEntryRenderer, } from './components/custom';
10
- export type { BodyProps, FooterProps, FormRef, GridSortModel, HeaderProps, ViewDetailsV2ContainerProps, } from './components/custom';
9
+ export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormContext, FormField, FormRenderer, FormRendererContainer, getReadableQuery, HistoryLog, MenuBar, MultiSelect, RecursiveEntryRenderer, RepeatableField, ResponsiveOverflow, RichTextViewer, UserAvatar, ViewDetailsV2Container, ViewOnlyEntryRenderer, parseMongoDB, difference, } from './components/custom';
10
+ export type { BodyProps, FooterProps, FormRef, GridSortModel, HeaderProps, MongoDBQueryValue, ViewDetailsV2ContainerProps, } from './components/custom';
11
11
  export { NumericFormat } from './components/custom/FormField/InputFieldComponent';
12
12
  export { Box, Container, Grid, Stack } from './components/layout';
13
13
  export * from './theme';