@evoke-platform/ui-components 1.14.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.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +2 -2
- package/dist/published/components/custom/CriteriaBuilder/types.d.ts +0 -15
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +0 -10
- package/dist/published/components/custom/CriteriaBuilder/utils.js +2 -161
- package/dist/published/components/custom/Form/utils.js +3 -2
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +131 -100
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
- package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +105 -185
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +18 -26
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +18 -18
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +96 -169
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +57 -13
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +61 -29
- package/dist/published/components/custom/FormV2/components/utils.d.ts +23 -4
- package/dist/published/components/custom/FormV2/components/utils.js +136 -26
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +28 -14
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +40 -46
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +56 -19
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
- package/dist/published/components/custom/index.d.ts +2 -0
- package/dist/published/components/custom/index.js +1 -0
- package/dist/published/components/custom/types.d.ts +15 -0
- package/dist/published/components/custom/types.js +1 -0
- package/dist/published/components/custom/util.d.ts +10 -0
- package/dist/published/components/custom/util.js +161 -1
- package/dist/published/index.d.ts +2 -2
- package/dist/published/index.js +1 -1
- 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, {
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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",
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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' });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { EntryRendererProps } from '../FormV2/components/types';
|
|
2
|
-
declare function ViewOnlyEntryRenderer(props: EntryRendererProps):
|
|
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,
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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,
|
|
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
|
-
|
|
136
|
-
React.createElement(
|
|
137
|
-
|
|
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((
|
|
71
|
-
setPanelLayout(
|
|
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';
|
|
@@ -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';
|
package/dist/published/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { ClickAwayListener, createTheme, darken, lighten, styled, Toolbar, useMe
|
|
|
2
2
|
export { CalendarPicker, DateTimePicker, MonthPicker, PickersDay, StaticDateTimePicker, StaticTimePicker, TimePicker, YearPicker, } from '@mui/x-date-pickers';
|
|
3
3
|
export * from './colors';
|
|
4
4
|
export * from './components/core';
|
|
5
|
-
export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormContext, FormField, FormRenderer, FormRendererContainer, getReadableQuery, HistoryLog, MenuBar, MultiSelect, RecursiveEntryRenderer, RepeatableField, ResponsiveOverflow, RichTextViewer, UserAvatar, ViewDetailsV2Container, ViewOnlyEntryRenderer, } from './components/custom';
|
|
5
|
+
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';
|
|
6
6
|
export { NumericFormat } from './components/custom/FormField/InputFieldComponent';
|
|
7
7
|
export { Box, Container, Grid, Stack } from './components/layout';
|
|
8
8
|
export * from './theme';
|