@evoke-platform/ui-components 1.17.0 → 1.18.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 (50) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +8 -4
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +238 -141
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +189 -67
  4. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
  5. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +12 -25
  6. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +4 -5
  7. package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +34 -22
  8. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +2 -11
  9. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +6 -34
  10. package/dist/published/components/custom/CriteriaBuilder/utils.js +18 -89
  11. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
  12. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
  13. package/dist/published/components/custom/Form/utils.d.ts +1 -0
  14. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +3 -1
  15. package/dist/published/components/custom/FormV2/FormRenderer.js +9 -4
  16. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +5 -0
  17. package/dist/published/components/custom/FormV2/FormRendererContainer.js +116 -53
  18. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -0
  19. package/dist/published/components/custom/FormV2/components/Body.js +4 -2
  20. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -0
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +46 -17
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +44 -11
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +41 -29
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
  30. package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +1 -0
  31. package/dist/published/components/custom/FormV2/components/FormletRenderer.js +6 -14
  32. package/dist/published/components/custom/FormV2/components/HtmlView.js +4 -0
  33. package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.d.ts +2 -0
  34. package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.js +15 -0
  35. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +18 -19
  36. package/dist/published/components/custom/FormV2/components/types.d.ts +8 -2
  37. package/dist/published/components/custom/FormV2/components/utils.d.ts +11 -8
  38. package/dist/published/components/custom/FormV2/components/utils.js +194 -78
  39. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +180 -2
  40. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +212 -0
  41. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +26 -10
  42. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +1 -0
  43. package/dist/published/components/custom/index.d.ts +1 -0
  44. package/dist/published/index.d.ts +1 -1
  45. package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
  46. package/dist/published/stories/FormRenderer.stories.d.ts +9 -3
  47. package/dist/published/stories/FormRenderer.stories.js +1 -0
  48. package/dist/published/stories/FormRendererContainer.stories.d.ts +25 -0
  49. package/dist/published/theme/hooks.d.ts +1 -0
  50. package/package.json +3 -2
@@ -8,6 +8,7 @@ import React from 'react';
8
8
  import { MemoryRouter } from 'react-router-dom';
9
9
  import { expect, it } from 'vitest';
10
10
  import FormRenderer from '../FormRenderer';
11
+ import { convertToReadOnly } from '../components/utils';
11
12
  import { accessibility508Object, createSpecialtyForm, jsonLogicDisplayTestSpecialtyForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, simpleConditionDisplayTestSpecialtyForm, specialtyObject, specialtyTypeObject, UpdateAccessibilityFormOne, UpdateAccessibilityFormTwo, users, } from './test-data';
12
13
  // Mock ResizeObserver
13
14
  global.ResizeObserver = class ResizeObserver {
@@ -1330,12 +1331,16 @@ describe('FormRenderer', () => {
1330
1331
  display: {
1331
1332
  label: 'Collection',
1332
1333
  createActionId: '_create',
1334
+ updateActionId: '_update',
1333
1335
  },
1334
1336
  },
1335
1337
  ],
1336
1338
  actionId: '_update',
1337
1339
  objectId: 'testObjectForCollections',
1338
1340
  };
1341
+ let collectionObject;
1342
+ let collectionObjectForm;
1343
+ let collectionObjectUpdateForm;
1339
1344
  let scrollIntoViewMock;
1340
1345
  let originalScrollIntoView;
1341
1346
  beforeEach(() => {
@@ -1364,7 +1369,7 @@ describe('FormRenderer', () => {
1364
1369
  ],
1365
1370
  };
1366
1371
  setupTestMocks(collectionFormObject, form);
1367
- const collectionObject = {
1372
+ collectionObject = {
1368
1373
  id: 'collectionObject',
1369
1374
  name: 'Collection Object',
1370
1375
  properties: [
@@ -1401,9 +1406,23 @@ describe('FormRenderer', () => {
1401
1406
  ],
1402
1407
  defaultFormId: 'collectionObjectForm',
1403
1408
  },
1409
+ {
1410
+ id: '_update',
1411
+ name: 'Update',
1412
+ type: 'update',
1413
+ outputEvent: 'updated',
1414
+ parameters: [
1415
+ {
1416
+ id: 'name',
1417
+ name: 'Name',
1418
+ type: 'string',
1419
+ },
1420
+ ],
1421
+ defaultFormId: 'collectionObjectUpdateForm',
1422
+ },
1404
1423
  ],
1405
1424
  };
1406
- const collectionObjectForm = {
1425
+ collectionObjectForm = {
1407
1426
  id: 'collectionObjectForm',
1408
1427
  name: 'Collection Object Form',
1409
1428
  entries: [
@@ -1431,6 +1450,26 @@ describe('FormRenderer', () => {
1431
1450
  },
1432
1451
  };
1433
1452
  setupTestMocks(collectionObject, collectionObjectForm);
1453
+ collectionObjectUpdateForm = {
1454
+ id: 'collectionObjectUpdateForm',
1455
+ name: 'Collection Object Update Form',
1456
+ entries: [
1457
+ {
1458
+ type: 'input',
1459
+ parameterId: 'name',
1460
+ display: {
1461
+ label: 'Name',
1462
+ required: true,
1463
+ },
1464
+ },
1465
+ ],
1466
+ actionId: '_update',
1467
+ objectId: 'collectionObject',
1468
+ display: {
1469
+ submitLabel: 'Update Collection Item',
1470
+ },
1471
+ };
1472
+ setupTestMocks(collectionObject, collectionObjectUpdateForm);
1434
1473
  });
1435
1474
  afterEach(() => {
1436
1475
  Element.prototype.scrollIntoView = originalScrollIntoView;
@@ -1528,6 +1567,48 @@ describe('FormRenderer', () => {
1528
1567
  await screen.findByRole('columnheader', { name: 'Name' });
1529
1568
  screen.getByRole('cell', { name: 'New Collection Item' });
1530
1569
  });
1570
+ it('should not send instance metadata fields when updating a collection item', async () => {
1571
+ const user = userEvent.setup();
1572
+ const updateRequestSpy = vitest.fn();
1573
+ const parentInstance = {
1574
+ id: 'testInstanceId',
1575
+ name: 'Test Instance',
1576
+ objectId: 'testObjectForCollections',
1577
+ };
1578
+ const existingCollectionItem = {
1579
+ id: 'existingCollectionItemId',
1580
+ version: 1,
1581
+ objectId: 'collectionObject',
1582
+ name: 'Existing Collection Item',
1583
+ relatedObject: {
1584
+ id: parentInstance.id,
1585
+ name: parentInstance.name,
1586
+ },
1587
+ };
1588
+ server.use(http.get('/api/data/objects/collectionObject/instances', () => HttpResponse.json([existingCollectionItem])), http.get('/api/data/objects/collectionObject/instances/existingCollectionItemId', () => HttpResponse.json(existingCollectionItem)), http.get('/api/data/objects/collectionObject/instances/existingCollectionItemId/object', () => HttpResponse.json(collectionObject)), http.get('/api/data/objects/collectionObject/instances/existingCollectionItemId/checkAccess', () => HttpResponse.json({ result: true })), http.post('/api/data/objects/collectionObject/instances/existingCollectionItemId/actions', async ({ request }) => {
1589
+ const body = (await request.json());
1590
+ updateRequestSpy(body);
1591
+ return HttpResponse.json({
1592
+ ...existingCollectionItem,
1593
+ name: body.input.name ?? existingCollectionItem.name,
1594
+ });
1595
+ }));
1596
+ render(React.createElement(FormRenderer, { form: form, instance: parentInstance, onChange: () => { } }));
1597
+ await screen.findByRole('cell', { name: 'Existing Collection Item' });
1598
+ await user.click(await screen.findByLabelText('edit-collection-instance-0'));
1599
+ await screen.findByRole('dialog');
1600
+ const nameField = await screen.findByRole('textbox', { name: /Name */i });
1601
+ await user.clear(nameField);
1602
+ await user.type(nameField, 'Updated Collection Item');
1603
+ await user.click(screen.getByRole('button', { name: 'Update Collection Item' }));
1604
+ await waitFor(() => expect(updateRequestSpy).toHaveBeenCalled());
1605
+ const body = updateRequestSpy.mock.calls[0][0];
1606
+ expect(body.actionId).toBe('_update');
1607
+ expect(body.input).toMatchObject({ name: 'Updated Collection Item' });
1608
+ expect(body.input).not.toHaveProperty('id');
1609
+ expect(body.input).not.toHaveProperty('objectId');
1610
+ expect(body.input).not.toHaveProperty('version');
1611
+ });
1531
1612
  it('should show validation errors', async () => {
1532
1613
  const user = userEvent.setup();
1533
1614
  render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
@@ -1691,4 +1772,101 @@ describe('FormRenderer', () => {
1691
1772
  await screen.findByLabelText('Date 2');
1692
1773
  });
1693
1774
  });
1775
+ describe('convertToReadOnly', () => {
1776
+ it('converts input entries to readonlyField entries', () => {
1777
+ const inputEntry = {
1778
+ type: 'input',
1779
+ parameterId: 'name',
1780
+ display: {
1781
+ label: 'Name',
1782
+ required: true,
1783
+ },
1784
+ };
1785
+ const result = convertToReadOnly(inputEntry);
1786
+ const display = inputEntry.display;
1787
+ expect(result).toEqual({
1788
+ type: 'readonlyField',
1789
+ propertyId: 'name',
1790
+ display,
1791
+ });
1792
+ });
1793
+ it('converts inputField entries to readonlyField entries', () => {
1794
+ const inputFieldEntry = {
1795
+ type: 'inputField',
1796
+ input: {
1797
+ id: 'description',
1798
+ type: 'string',
1799
+ },
1800
+ display: {
1801
+ label: 'Description',
1802
+ },
1803
+ };
1804
+ const result = convertToReadOnly(inputFieldEntry);
1805
+ const display = inputFieldEntry.display;
1806
+ expect(result).toEqual({
1807
+ type: 'readonlyField',
1808
+ propertyId: 'description',
1809
+ display,
1810
+ });
1811
+ });
1812
+ it('recursively converts nested section and column entries', () => {
1813
+ const nestedEntry = {
1814
+ type: 'sections',
1815
+ sections: [
1816
+ {
1817
+ entries: [
1818
+ {
1819
+ type: 'input',
1820
+ parameterId: 'firstName',
1821
+ display: {
1822
+ label: 'First Name',
1823
+ },
1824
+ },
1825
+ {
1826
+ type: 'columns',
1827
+ columns: [
1828
+ {
1829
+ entries: [
1830
+ {
1831
+ type: 'inputField',
1832
+ input: {
1833
+ id: 'lastName',
1834
+ type: 'string',
1835
+ },
1836
+ display: {
1837
+ label: 'Last Name',
1838
+ },
1839
+ },
1840
+ ],
1841
+ },
1842
+ ],
1843
+ },
1844
+ ],
1845
+ },
1846
+ ],
1847
+ };
1848
+ const result = convertToReadOnly(nestedEntry);
1849
+ expect(result.sections[0].entries?.[0]).toMatchObject({
1850
+ type: 'readonlyField',
1851
+ propertyId: 'firstName',
1852
+ });
1853
+ expect(result.sections[0].entries?.[1]).toMatchObject({
1854
+ type: 'columns',
1855
+ });
1856
+ const nestedColumnEntry = (result.sections[0].entries?.[1])
1857
+ .columns[0].entries?.[0];
1858
+ expect(nestedColumnEntry).toMatchObject({
1859
+ type: 'readonlyField',
1860
+ propertyId: 'lastName',
1861
+ });
1862
+ });
1863
+ it('returns unsupported entry types unchanged', () => {
1864
+ const contentEntry = {
1865
+ type: 'content',
1866
+ html: '<div>Read only content</div>',
1867
+ };
1868
+ const result = convertToReadOnly(contentEntry);
1869
+ expect(result).toBe(contentEntry);
1870
+ });
1871
+ });
1694
1872
  });
@@ -116,6 +116,73 @@ describe('FormRendererContainer', () => {
116
116
  });
117
117
  });
118
118
  });
119
+ it('keeps the rich text editor mounted while typing when autosave is not configured', async () => {
120
+ const user = userEvent.setup();
121
+ const mountSpy = vi.fn();
122
+ const richTextForm = {
123
+ id: 'richTextForm',
124
+ name: 'Rich Text Form',
125
+ actionId: '_update',
126
+ objectId: 'richTextObject',
127
+ entries: [
128
+ {
129
+ parameterId: 'notes',
130
+ type: 'input',
131
+ display: {
132
+ label: 'Notes',
133
+ },
134
+ },
135
+ ],
136
+ };
137
+ const richTextObject = {
138
+ id: 'richTextObject',
139
+ name: 'Rich Text Object',
140
+ actions: [
141
+ {
142
+ id: '_update',
143
+ name: 'Update',
144
+ type: 'update',
145
+ parameters: [
146
+ {
147
+ id: 'notes',
148
+ name: 'Notes',
149
+ type: 'richText',
150
+ },
151
+ ],
152
+ outputEvent: 'updated',
153
+ },
154
+ ],
155
+ properties: [
156
+ {
157
+ id: 'notes',
158
+ name: 'Notes',
159
+ type: 'richText',
160
+ },
161
+ ],
162
+ };
163
+ const TestRichTextEditor = (props) => {
164
+ React.useEffect(() => {
165
+ mountSpy();
166
+ }, []);
167
+ return (React.createElement("textarea", { "aria-label": "Notes", value: props.value || '', onChange: (event) => props.handleUpdate?.(event.target.value), onBlur: () => props.onBlur?.() }));
168
+ };
169
+ server.use(http.get('/api/data/objects/richTextObject/effective', () => HttpResponse.json(richTextObject)), http.get('/api/data/objects/richTextObject/instances/test-instance', () => {
170
+ return HttpResponse.json({
171
+ id: 'test-instance',
172
+ name: 'Rich Text Instance',
173
+ notes: '',
174
+ });
175
+ }), http.get('/api/data/objects/richTextObject/instances/test-instance/object', () => {
176
+ return HttpResponse.json(richTextObject);
177
+ }), http.get('/api/data/forms/richTextForm/effective', () => HttpResponse.json(richTextForm)));
178
+ render(React.createElement(FormRendererContainer, { objectId: "richTextObject", formId: "richTextForm", dataType: "objectInstances", actionId: "_update", instanceId: "test-instance", richTextEditor: TestRichTextEditor }));
179
+ const notesField = await screen.findByRole('textbox', { name: 'Notes' });
180
+ await user.click(notesField);
181
+ await user.type(notesField, 'Focus should stay here');
182
+ const updatedNotesField = await screen.findByRole('textbox', { name: 'Notes' });
183
+ expect(updatedNotesField).toHaveFocus();
184
+ expect(mountSpy).toHaveBeenCalledTimes(1);
185
+ });
119
186
  describe('autosave functionality', () => {
120
187
  it('should trigger autosave when field loses focus', async () => {
121
188
  const user = userEvent.setup();
@@ -387,6 +454,10 @@ describe('FormRendererContainer', () => {
387
454
  render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
388
455
  const cityField = await screen.findByRole('textbox', { name: 'City' });
389
456
  await user.clear(cityField);
457
+ // react state may not have finished updating state internally yet, so wait for the field to be cleared before proceeding
458
+ await waitFor(() => {
459
+ expect(cityField).toHaveValue('');
460
+ });
390
461
  await user.type(cityField, 'Cambridge');
391
462
  await user.tab();
392
463
  await waitFor(() => {
@@ -758,6 +829,81 @@ describe('FormRendererContainer', () => {
758
829
  // Verify autosave was not triggered
759
830
  expect(autosaveActionSpy).not.toHaveBeenCalled();
760
831
  });
832
+ it('should trigger autosave when a rich text field loses focus', async () => {
833
+ const user = userEvent.setup();
834
+ const autosaveActionSpy = vi.fn();
835
+ const richTextForm = {
836
+ id: 'richTextAutosaveForm',
837
+ name: 'Rich Text Autosave Form',
838
+ objectId: 'richTextObject',
839
+ actionId: '_update',
840
+ autosaveActionId: '_autosave',
841
+ entries: [
842
+ {
843
+ parameterId: 'notes',
844
+ type: 'input',
845
+ display: {
846
+ label: 'Notes',
847
+ },
848
+ },
849
+ ],
850
+ };
851
+ const richTextObject = {
852
+ id: 'richTextObject',
853
+ name: 'Rich Text Object',
854
+ actions: [
855
+ {
856
+ id: '_update',
857
+ name: 'Update',
858
+ type: 'update',
859
+ parameters: [
860
+ {
861
+ id: 'notes',
862
+ name: 'Notes',
863
+ type: 'richText',
864
+ },
865
+ ],
866
+ outputEvent: 'updated',
867
+ },
868
+ ],
869
+ properties: [
870
+ {
871
+ id: 'notes',
872
+ name: 'Notes',
873
+ type: 'richText',
874
+ },
875
+ ],
876
+ };
877
+ const TestRichTextEditor = (props) => {
878
+ return (React.createElement("textarea", { "aria-label": "Notes", value: props.value || '', onChange: (event) => props.handleUpdate?.(event.target.value), onBlur: () => props.onBlur?.() }));
879
+ };
880
+ server.use(http.get('/api/data/objects/richTextObject/effective', () => HttpResponse.json(richTextObject)), http.get('/api/data/objects/richTextObject/instances/test-instance', () => {
881
+ return HttpResponse.json({
882
+ id: 'test-instance',
883
+ name: 'Rich Text Instance',
884
+ notes: '',
885
+ });
886
+ }), http.get('/api/data/objects/richTextObject/instances/test-instance/object', () => {
887
+ return HttpResponse.json(richTextObject);
888
+ }), http.get('/api/data/forms/richTextAutosaveForm/effective', () => HttpResponse.json(richTextForm)), http.post('/api/data/objects/richTextObject/instances/test-instance/actions', async ({ request }) => {
889
+ const body = (await request.json());
890
+ autosaveActionSpy(body);
891
+ return HttpResponse.json({
892
+ id: 'test-instance',
893
+ ...body.input,
894
+ });
895
+ }));
896
+ render(React.createElement(FormRendererContainer, { objectId: "richTextObject", formId: "richTextAutosaveForm", dataType: "objectInstances", actionId: "_update", instanceId: "test-instance", richTextEditor: TestRichTextEditor }));
897
+ const notesField = await screen.findByRole('textbox', { name: 'Notes' });
898
+ await user.type(notesField, 'Autosave me');
899
+ await user.tab();
900
+ expect(autosaveActionSpy).toHaveBeenCalledWith(expect.objectContaining({
901
+ actionId: '_autosave',
902
+ input: expect.objectContaining({
903
+ notes: expect.stringContaining('Autosave me'),
904
+ }),
905
+ }));
906
+ });
761
907
  it('should trigger autosave immediately when a radio button is selected', async () => {
762
908
  const user = userEvent.setup();
763
909
  const autosaveActionSpy = vi.fn();
@@ -1420,6 +1566,72 @@ describe('FormRendererContainer', () => {
1420
1566
  await user.click(discardButton);
1421
1567
  await waitFor(() => expect(firstNameInput).toHaveValue(''));
1422
1568
  });
1569
+ it('should reset rich text content without remounting the editor when discarding changes', async () => {
1570
+ const user = userEvent.setup();
1571
+ const mountSpy = vi.fn();
1572
+ const form = {
1573
+ id: 'richTextDiscardForm',
1574
+ name: 'Rich Text Form',
1575
+ entries: [
1576
+ {
1577
+ type: 'input',
1578
+ parameterId: 'notes',
1579
+ display: {
1580
+ label: 'Notes',
1581
+ },
1582
+ },
1583
+ ],
1584
+ actionId: '_update',
1585
+ objectId: 'richTextDiscardObject',
1586
+ };
1587
+ const richTextObject = {
1588
+ id: 'richTextDiscardObject',
1589
+ name: 'Rich Text Object',
1590
+ actions: [
1591
+ {
1592
+ id: '_update',
1593
+ name: 'Update',
1594
+ type: 'update',
1595
+ parameters: [
1596
+ {
1597
+ id: 'notes',
1598
+ name: 'Notes',
1599
+ type: 'richText',
1600
+ },
1601
+ ],
1602
+ outputEvent: 'updated',
1603
+ },
1604
+ ],
1605
+ properties: [
1606
+ {
1607
+ id: 'notes',
1608
+ name: 'Notes',
1609
+ type: 'richText',
1610
+ },
1611
+ ],
1612
+ };
1613
+ const TestRichTextEditor = (props) => {
1614
+ React.useEffect(() => {
1615
+ mountSpy();
1616
+ }, []);
1617
+ return (React.createElement("textarea", { "aria-label": "Notes", value: props.value || '', onChange: (event) => props.handleUpdate?.(event.target.value), onBlur: () => props.onBlur?.() }));
1618
+ };
1619
+ server.use(http.get('/api/data/objects/richTextDiscardObject/effective', () => HttpResponse.json(richTextObject)), http.get('/api/data/forms/richTextDiscardForm/effective', () => HttpResponse.json(form)), http.get('/api/data/objects/richTextDiscardObject/instances/test-instance', () => HttpResponse.json({
1620
+ id: 'test-instance',
1621
+ name: 'Rich Text Instance',
1622
+ notes: 'Original notes',
1623
+ })), http.get('/api/data/objects/richTextDiscardObject/instances/test-instance/object', () => HttpResponse.json(richTextObject)));
1624
+ render(React.createElement(FormRendererContainer, { objectId: "richTextDiscardObject", formId: "richTextDiscardForm", dataType: "objectInstances", actionId: "_update", instanceId: "test-instance", richTextEditor: TestRichTextEditor }));
1625
+ const notesField = await screen.findByRole('textbox', { name: 'Notes' });
1626
+ expect(notesField).toHaveValue('{\\rtf1\\ansi\\deff0 {\\fonttbl {\\f0 Calibri;}} \\f0\\fs22 Original notes}');
1627
+ await user.clear(notesField);
1628
+ await user.type(notesField, 'Updated notes');
1629
+ expect(notesField).toHaveValue('Updated notes');
1630
+ const discardButton = await screen.findByRole('button', { name: 'Discard Changes' });
1631
+ await user.click(discardButton);
1632
+ await waitFor(() => expect(notesField).toHaveValue('{\\rtf1\\ansi\\deff0 {\\fonttbl {\\f0 Calibri;}} \\f0\\fs22 Original notes}'));
1633
+ expect(mountSpy).toHaveBeenCalledTimes(1);
1634
+ });
1423
1635
  it('should show a not found error if the instance cannot be found', async () => {
1424
1636
  const form = {
1425
1637
  id: 'simpleForm',
@@ -14,10 +14,13 @@ import DropdownRepeatableField from '../FormV2/components/FormFieldTypes/Collect
14
14
  import RepeatableField from '../FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField';
15
15
  import Criteria from '../FormV2/components/FormFieldTypes/Criteria';
16
16
  import { Document } from '../FormV2/components/FormFieldTypes/DocumentFiles/Document';
17
+ import { FileContent } from '../FormV2/components/FormFieldTypes/FileContent';
17
18
  import { Image } from '../FormV2/components/FormFieldTypes/Image';
18
19
  import PropertyProtection from '../FormV2/components/PropertyProtection';
19
20
  import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getDefaultPages, isAddressProperty, } from '../FormV2/components/utils';
20
21
  import HtmlView from '../FormV2/components/HtmlView';
22
+ import FormletRenderer from '../FormV2/components/FormletRenderer';
23
+ import MisconfiguredErrorMessage from '../FormV2/components/MisconfiguredErrorMessage';
21
24
  function ViewOnlyEntryRenderer(props) {
22
25
  const { entry } = props;
23
26
  const { fetchedOptions, setFetchedOptions, object, instance, richTextEditor: RichTextEditor } = useFormContext();
@@ -28,6 +31,7 @@ function ViewOnlyEntryRenderer(props) {
28
31
  const [currentDisplayValue, setCurrentDisplayValue] = useState(instance?.[entryId]);
29
32
  const [protectionMode, setProtectionMode] = useState('mask');
30
33
  const display = 'display' in entry ? entry.display : undefined;
34
+ const isReference = (value) => !!value && typeof value === 'object' && 'id' in value && 'name' in value;
31
35
  const fieldDefinition = useMemo(() => {
32
36
  const def = entry.type === 'readonlyField'
33
37
  ? isAddressProperty(entry.propertyId)
@@ -110,14 +114,14 @@ function ViewOnlyEntryRenderer(props) {
110
114
  return (React.createElement(AddressFields, { entry: entry, viewOnly: true, entryId: entryId, fieldDefinition: fieldDefinition }));
111
115
  }
112
116
  else {
113
- let fieldValue = currentDisplayValue ?? (instance?.[entryId] || '');
117
+ let fieldValue = currentDisplayValue ?? instance?.[entryId];
114
118
  switch (fieldDefinition?.type) {
115
119
  case 'object':
116
- if (navigationSlug && fieldDefinition?.objectId) {
120
+ if (navigationSlug && fieldDefinition?.objectId && isReference(fieldValue)) {
117
121
  return (React.createElement(FieldWrapper, { inputId: entryId, inputType: "object", label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
118
122
  React.createElement(Link, { sx: { cursor: 'pointer', fontFamily: 'sans-serif' }, href: `${'/app'}${navigationSlug.replace(':instanceId', fieldValue.id)}` }, fieldValue.name)));
119
123
  }
120
- fieldValue = fieldValue.name;
124
+ fieldValue = isReference(fieldValue) ? fieldValue.name : undefined;
121
125
  break;
122
126
  case 'array':
123
127
  if (!isEmpty(fieldValue)) {
@@ -128,24 +132,29 @@ function ViewOnlyEntryRenderer(props) {
128
132
  }
129
133
  break;
130
134
  case 'user':
131
- fieldValue = fieldValue && fieldValue.name;
135
+ fieldValue = isReference(fieldValue) ? fieldValue.name : undefined;
132
136
  break;
133
137
  case 'date':
134
138
  fieldValue =
135
139
  isProtectedProperty && protectionMode === 'mask'
136
140
  ? fieldValue
137
- : fieldValue && DateTime.fromISO(fieldValue).toFormat('MM/dd/yyyy');
141
+ : typeof fieldValue === 'string'
142
+ ? DateTime.fromISO(fieldValue).toFormat('MM/dd/yyyy')
143
+ : undefined;
138
144
  break;
139
145
  case 'date-time':
140
146
  fieldValue =
141
147
  fieldValue && fieldValue instanceof Date
142
148
  ? DateTime.fromJSDate(fieldValue).toFormat('MM/dd/yyyy hh:mm a')
143
- : fieldValue && DateTime.fromISO(fieldValue).toFormat('MM/dd/yyyy hh:mm a');
149
+ : typeof fieldValue === 'string'
150
+ ? DateTime.fromISO(fieldValue).toFormat('MM/dd/yyyy hh:mm a')
151
+ : undefined;
144
152
  break;
145
153
  case 'time':
146
154
  fieldValue =
147
- fieldValue &&
148
- DateTime.fromISO(DateTime.now().toISODate() + 'T' + fieldValue).toFormat('hh:mm a');
155
+ typeof fieldValue === 'string'
156
+ ? DateTime.fromISO(`${DateTime.now().toISODate()}T${fieldValue}`).toFormat('hh:mm a')
157
+ : undefined;
149
158
  break;
150
159
  default:
151
160
  break;
@@ -164,7 +173,11 @@ function ViewOnlyEntryRenderer(props) {
164
173
  }
165
174
  else if (fieldDefinition.type === 'document' || fieldDefinition.type === 'file') {
166
175
  return (React.createElement(FieldWrapper, { inputId: entryId, inputType: fieldDefinition.type, label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, prefix: display?.prefix, suffix: display?.suffix, viewOnly: true },
167
- React.createElement(Document, { id: entryId, error: false, value: fieldValue, canUpdateProperty: false })));
176
+ React.createElement(Document, { id: entryId, fieldType: fieldDefinition.type, error: false, value: fieldValue, canUpdateProperty: false })));
177
+ }
178
+ else if (fieldDefinition.type === 'fileContent') {
179
+ return (React.createElement(FieldWrapper, { inputId: entryId, inputType: 'fileContent', label: display?.label || fieldDefinition?.name || 'default', value: fieldValue, required: display?.required || false, viewOnly: true },
180
+ React.createElement(FileContent, { id: entryId, error: false, value: fieldValue, canUpdateProperty: false })));
168
181
  }
169
182
  else if (fieldDefinition.type === 'collection') {
170
183
  if (fieldDefinition.manyToManyPropertyId && !isEmpty(middleObject)) {
@@ -195,6 +208,9 @@ function ViewOnlyEntryRenderer(props) {
195
208
  const filteredEntry = filterEmptySections(entry, instance);
196
209
  return filteredEntry ? React.createElement(AccordionSections, { entry: filteredEntry, readOnly: true }) : null;
197
210
  }
198
- return null;
211
+ else if (entry.type === 'formlet') {
212
+ return React.createElement(FormletRenderer, { entry: entry, readOnly: true });
213
+ }
214
+ return React.createElement(MisconfiguredErrorMessage, null);
199
215
  }
200
216
  export default ViewOnlyEntryRenderer;
@@ -131,6 +131,7 @@ function ViewDetailsV2ContainerInner(props) {
131
131
  onCollapseAll: handleCollapseAll,
132
132
  expandedSections,
133
133
  hasAccordions: hasSections,
134
+ readOnly: true,
134
135
  })
135
136
  : updatedEntries.map((entry, index) => (React.createElement(ViewOnlyEntryRenderer, { entry: entry, key: index }))))))) : (React.createElement(Box, { sx: { padding: '20px' } },
136
137
  React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
@@ -16,5 +16,6 @@ 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 type { TreeViewObject, TreeViewProperty } from './CriteriaBuilder/types';
19
20
  export * from './util';
20
21
  export type { MongoDBQueryValue } from './types';
@@ -7,7 +7,7 @@ export { CalendarPicker, DateTimePicker, MonthPicker, PickersDay, StaticDateTime
7
7
  export * from './colors';
8
8
  export * from './components/core';
9
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';
10
+ export type { BodyProps, FooterProps, FormRef, GridSortModel, HeaderProps, MongoDBQueryValue, ViewDetailsV2ContainerProps, TreeViewObject, TreeViewProperty, } 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';