@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.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +8 -4
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +238 -141
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +189 -67
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +6 -6
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +12 -25
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.d.ts +4 -5
- package/dist/published/components/custom/CriteriaBuilder/PropertyTreeItem.js +34 -22
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +2 -11
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +6 -34
- package/dist/published/components/custom/CriteriaBuilder/utils.js +18 -89
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/DocumentList.js +6 -3
- package/dist/published/components/custom/Form/utils.d.ts +1 -0
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +3 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +9 -4
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +5 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +116 -53
- package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Body.js +4 -2
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +46 -17
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.d.ts +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +44 -11
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +4 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +41 -29
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.d.ts +12 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/FileContent.js +197 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +14 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/FormletRenderer.js +6 -14
- package/dist/published/components/custom/FormV2/components/HtmlView.js +4 -0
- package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/MisconfiguredErrorMessage.js +15 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +18 -19
- package/dist/published/components/custom/FormV2/components/types.d.ts +8 -2
- package/dist/published/components/custom/FormV2/components/utils.d.ts +11 -8
- package/dist/published/components/custom/FormV2/components/utils.js +194 -78
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +180 -2
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +212 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +26 -10
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +1 -0
- package/dist/published/components/custom/index.d.ts +1 -0
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +70 -22
- package/dist/published/stories/FormRenderer.stories.d.ts +9 -3
- package/dist/published/stories/FormRenderer.stories.js +1 -0
- package/dist/published/stories/FormRendererContainer.stories.d.ts +25 -0
- package/dist/published/theme/hooks.d.ts +1 -0
- 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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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()
|
|
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
|
-
|
|
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';
|