@evoke-platform/ui-components 1.10.0-testing.2 → 1.10.0-testing.20

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 (64) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +1 -1
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +430 -0
  4. package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
  5. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  6. package/dist/published/components/custom/Form/utils.js +1 -0
  7. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +14 -1
  8. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  9. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  10. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  11. package/dist/published/components/custom/FormV2/FormRenderer.js +17 -4
  12. package/dist/published/components/custom/FormV2/FormRendererContainer.js +112 -73
  13. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  14. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  15. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  16. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  17. package/dist/published/components/custom/FormV2/components/Footer.js +3 -3
  18. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +1 -1
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +0 -3
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +31 -48
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +16 -4
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +109 -81
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +38 -16
  32. package/dist/published/components/custom/FormV2/components/Header.d.ts +13 -3
  33. package/dist/published/components/custom/FormV2/components/Header.js +47 -8
  34. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +44 -35
  35. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  36. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -0
  37. package/dist/published/components/custom/FormV2/components/utils.d.ts +2 -2
  38. package/dist/published/components/custom/FormV2/components/utils.js +11 -14
  39. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +432 -4
  40. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +642 -13
  41. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  42. package/dist/published/components/custom/FormV2/tests/test-data.js +140 -0
  43. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  44. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +155 -0
  45. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  46. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +140 -0
  47. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  48. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  49. package/dist/published/components/custom/index.d.ts +2 -0
  50. package/dist/published/components/custom/index.js +1 -0
  51. package/dist/published/index.d.ts +6 -6
  52. package/dist/published/index.js +1 -1
  53. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  54. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  55. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  56. package/dist/published/stories/FormRendererData.d.ts +12 -0
  57. package/dist/published/stories/FormRendererData.js +27 -44
  58. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  59. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  60. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  61. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  62. package/dist/published/stories/sharedMswHandlers.js +49 -10
  63. package/dist/published/theme/hooks.d.ts +4 -3
  64. package/package.json +4 -2
@@ -248,7 +248,7 @@ const customDelete = (props) => {
248
248
  '&:hover': { backgroundColor: 'transparent', boxShadow: 'none' },
249
249
  color: '#212B36',
250
250
  fontWeight: '400',
251
- } }, "Delete group")) : (React.createElement(IconButton, { onClick: handleOnClick, size: "small" },
251
+ } }, "Delete group")) : (React.createElement(IconButton, { onClick: handleOnClick, size: "small", "aria-label": "Delete rule" },
252
252
  React.createElement(TrashCan, { sx: { ':hover': { color: '#637381' } } })))) : (React.createElement(React.Fragment, null));
253
253
  };
254
254
  export const valueEditor = (props) => {
@@ -0,0 +1,430 @@
1
+ import * as matchers from '@testing-library/jest-dom/matchers';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import React from 'react';
5
+ import { expect, it } from 'vitest';
6
+ import CriteriaBuilder from './CriteriaBuilder';
7
+ expect.extend(matchers);
8
+ const mockProperties = [
9
+ {
10
+ id: 'name',
11
+ name: 'Name',
12
+ type: 'string',
13
+ },
14
+ {
15
+ id: 'age',
16
+ name: 'Age',
17
+ type: 'integer',
18
+ },
19
+ {
20
+ id: 'birthDate',
21
+ name: 'Birth Date',
22
+ type: 'date',
23
+ },
24
+ {
25
+ id: 'createdTime',
26
+ name: 'Created Time',
27
+ type: 'time',
28
+ },
29
+ {
30
+ id: 'tags',
31
+ name: 'Tags',
32
+ type: 'array',
33
+ enum: ['tag1', 'tag2', 'tag3'],
34
+ },
35
+ {
36
+ id: 'status',
37
+ name: 'Status',
38
+ type: 'string',
39
+ enum: ['active', 'inactive', 'pending'],
40
+ },
41
+ {
42
+ id: 'profilePic',
43
+ name: 'Profile Picture',
44
+ type: 'image',
45
+ },
46
+ {
47
+ id: 'metadata',
48
+ name: 'Metadata',
49
+ type: 'document',
50
+ },
51
+ {
52
+ id: 'percentCompleted',
53
+ name: 'Percent Completed',
54
+ type: 'number',
55
+ },
56
+ {
57
+ id: 'boolean',
58
+ name: 'Boolean',
59
+ type: 'boolean',
60
+ },
61
+ ];
62
+ describe('CriteriaBuilder', () => {
63
+ // Mock function for setCriteria
64
+ const setCriteriaMock = vi.fn();
65
+ beforeEach(() => {
66
+ // Reset the mock before each test
67
+ setCriteriaMock.mockReset();
68
+ });
69
+ describe('when passed single-select fields', () => {
70
+ it('should render the field name', () => {
71
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
72
+ status: 'active',
73
+ }, setCriteria: setCriteriaMock }));
74
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Status');
75
+ });
76
+ it('should render the "is" operator to represent equality', () => {
77
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
78
+ status: 'active',
79
+ }, setCriteria: setCriteriaMock }));
80
+ expect(screen.getByRole('combobox', { name: /select operator/i })).toHaveValue('Is');
81
+ });
82
+ ['In', 'Is', 'Is empty', 'Is not', 'Is not empty', 'Not in'].forEach((operator) => {
83
+ it(`should offer the ${operator} operator`, async () => {
84
+ const user = userEvent.setup();
85
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
86
+ status: 'active',
87
+ }, setCriteria: setCriteriaMock }));
88
+ await user.click(screen.getByRole('combobox', { name: /select operator/i }));
89
+ await screen.findByRole('option', { name: operator });
90
+ });
91
+ });
92
+ describe('when utilizing the "is" operator', () => {
93
+ it('should display a single preloaded value', async () => {
94
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
95
+ status: 'active',
96
+ }, setCriteria: setCriteriaMock }));
97
+ expect(screen.getByRole('combobox', { name: 'Select or enter a value' })).toHaveValue('active');
98
+ });
99
+ it('should hide list of values after selecting a value', async () => {
100
+ const user = userEvent.setup();
101
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
102
+ status: '',
103
+ }, setCriteria: setCriteriaMock }));
104
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
105
+ await user.click(valueInput);
106
+ const listbox = await screen.findByRole('listbox');
107
+ await user.click(screen.getByRole('option', { name: 'active' }));
108
+ expect(listbox).not.toBeInTheDocument();
109
+ });
110
+ it('should mark the selected value in the list', async () => {
111
+ const user = userEvent.setup();
112
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
113
+ status: 'active',
114
+ }, setCriteria: setCriteriaMock }));
115
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
116
+ await user.click(valueInput);
117
+ const selectedOption = await screen.findByRole('option', { name: 'active' });
118
+ expect(selectedOption).toHaveAttribute('aria-selected', 'true');
119
+ });
120
+ it('should allow values to be deleted using the backspace key', async () => {
121
+ const user = userEvent.setup();
122
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
123
+ status: 'active',
124
+ }, setCriteria: setCriteriaMock }));
125
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
126
+ await user.click(valueInput);
127
+ for (let i = 0; i < 6; i++) {
128
+ await user.type(valueInput, '{Backspace}');
129
+ }
130
+ // Clear focus away from input
131
+ await user.click(screen.getByRole('combobox', { name: /select operator/i }));
132
+ expect(screen.getByRole('combobox', { name: /select or enter a value/i })).toHaveValue('');
133
+ });
134
+ });
135
+ });
136
+ describe('when passed multi-select (array) fields', () => {
137
+ it('should render the field name', () => {
138
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
139
+ tags: { $in: ['tag1', 'tag2'] },
140
+ }, setCriteria: setCriteriaMock }));
141
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Tags');
142
+ });
143
+ it('should load the correct operator for pre-loaded data', () => {
144
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
145
+ tags: { $in: ['tag1', 'tag2'] },
146
+ }, setCriteria: setCriteriaMock }));
147
+ expect(screen.getByRole('combobox', { name: /select operator/i })).toHaveValue('In');
148
+ });
149
+ ['In', 'Is empty', 'Is not empty'].forEach((operator) => {
150
+ it(`should offer the ${operator} operator`, async () => {
151
+ const user = userEvent.setup();
152
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
153
+ tags: { $in: ['tag1', 'tag2'] },
154
+ }, setCriteria: setCriteriaMock }));
155
+ await user.click(screen.getByRole('combobox', { name: /select operator/i }));
156
+ await screen.findByRole('option', { name: operator });
157
+ });
158
+ });
159
+ it('should display a single preloaded value', async () => {
160
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
161
+ tags: { $in: ['tag1'] },
162
+ }, setCriteria: setCriteriaMock }));
163
+ screen.getByRole('button', { name: 'tag1' });
164
+ });
165
+ it('should display multiple preloaded values', async () => {
166
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
167
+ tags: { $in: ['tag1', 'tag2'] },
168
+ }, setCriteria: setCriteriaMock }));
169
+ screen.getByRole('button', { name: 'tag1' });
170
+ screen.getByRole('button', { name: 'tag2' });
171
+ });
172
+ it('should hide list of values open after selecting a value', async () => {
173
+ const user = userEvent.setup();
174
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
175
+ tags: { $in: [] },
176
+ }, setCriteria: setCriteriaMock }));
177
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
178
+ await user.click(valueInput);
179
+ const listbox = await screen.findByRole('listbox');
180
+ await user.click(screen.getByRole('option', { name: 'tag1' }));
181
+ expect(listbox).not.toBeInTheDocument();
182
+ });
183
+ it('should mark the selected values in the list', async () => {
184
+ const user = userEvent.setup();
185
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
186
+ tags: { $in: ['tag1', 'tag2'] },
187
+ }, setCriteria: setCriteriaMock }));
188
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
189
+ await user.click(valueInput);
190
+ const selectedOption1 = await screen.findByRole('option', { name: 'tag1' });
191
+ const selectedOption2 = await screen.findByRole('option', { name: 'tag2' });
192
+ expect(selectedOption1).toHaveAttribute('aria-selected', 'true');
193
+ expect(selectedOption2).toHaveAttribute('aria-selected', 'true');
194
+ });
195
+ });
196
+ describe('when passed (decimal) number fields', () => {
197
+ it('should render the field name', () => {
198
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
199
+ percentCompleted: 30.5,
200
+ }, setCriteria: setCriteriaMock }));
201
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Percent Completed');
202
+ });
203
+ [
204
+ 'Greater than',
205
+ 'Greater than or equal to',
206
+ 'In',
207
+ 'Is',
208
+ 'Is empty',
209
+ 'Is not empty',
210
+ 'Less than',
211
+ 'Less than or equal to',
212
+ 'Not in',
213
+ 'Is not',
214
+ ].forEach((operator) => {
215
+ it(`should offer the ${operator} operator`, async () => {
216
+ const user = userEvent.setup();
217
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
218
+ percentCompleted: 25.5,
219
+ }, setCriteria: setCriteriaMock }));
220
+ await user.click(screen.getByRole('combobox', { name: /select operator/i }));
221
+ await screen.findByRole('option', { name: operator });
222
+ });
223
+ });
224
+ describe('when utilizing the "in" operator', () => {
225
+ it('should allow numbers to be entered', async () => {
226
+ const user = userEvent.setup();
227
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
228
+ percentCompleted: { $in: [] },
229
+ }, setCriteria: setCriteriaMock }));
230
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
231
+ await user.click(valueInput);
232
+ await user.type(valueInput, '4{Enter}');
233
+ screen.getByRole('button', { name: '4' });
234
+ });
235
+ it('should not allow non-numeric values to be entered', async () => {
236
+ const user = userEvent.setup();
237
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
238
+ percentCompleted: { $in: [] },
239
+ }, setCriteria: setCriteriaMock }));
240
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
241
+ await user.click(valueInput);
242
+ await user.type(valueInput, 'nonNumericValue{Enter}');
243
+ expect(screen.queryByText('nonNumericValue')).not.toBeInTheDocument();
244
+ });
245
+ });
246
+ describe('when utilizing the "not in" operator', () => {
247
+ it('should allow numbers to be entered', async () => {
248
+ const user = userEvent.setup();
249
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
250
+ age: { $nin: [] },
251
+ }, setCriteria: setCriteriaMock }));
252
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
253
+ await user.click(valueInput);
254
+ await user.type(valueInput, '4{Enter}');
255
+ screen.getByRole('button', { name: '4' });
256
+ });
257
+ it('should not allow non-numeric values to be entered', async () => {
258
+ const user = userEvent.setup();
259
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
260
+ age: { $nin: [] },
261
+ }, setCriteria: setCriteriaMock }));
262
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
263
+ await user.click(valueInput);
264
+ await user.type(valueInput, 'nonNumericValue{Enter}');
265
+ expect(screen.queryByText('nonNumericValue')).not.toBeInTheDocument();
266
+ });
267
+ });
268
+ });
269
+ describe('when passed integer fields', () => {
270
+ it('should render the field name', () => {
271
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
272
+ age: 25,
273
+ }, setCriteria: setCriteriaMock }));
274
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Age');
275
+ });
276
+ [
277
+ 'Greater than',
278
+ 'Greater than or equal to',
279
+ 'In',
280
+ 'Is',
281
+ 'Is empty',
282
+ 'Is not empty',
283
+ 'Less than',
284
+ 'Less than or equal to',
285
+ 'Not in',
286
+ 'Is not',
287
+ ].forEach((operator) => {
288
+ it(`should offer the ${operator} operator`, async () => {
289
+ const user = userEvent.setup();
290
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
291
+ age: 25,
292
+ }, setCriteria: setCriteriaMock }));
293
+ await user.click(screen.getByRole('combobox', { name: /select operator/i }));
294
+ await screen.findByRole('option', { name: operator });
295
+ });
296
+ });
297
+ describe('when utilizing the "in" operator', () => {
298
+ it('should allow numbers to be entered', async () => {
299
+ const user = userEvent.setup();
300
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
301
+ age: { $in: [] },
302
+ }, setCriteria: setCriteriaMock }));
303
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
304
+ await user.click(valueInput);
305
+ await user.type(valueInput, '4{Enter}');
306
+ screen.getByRole('button', { name: '4' });
307
+ });
308
+ it('should not allow non-numeric values to be entered', async () => {
309
+ const user = userEvent.setup();
310
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
311
+ age: { $in: [] },
312
+ }, setCriteria: setCriteriaMock }));
313
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
314
+ await user.click(valueInput);
315
+ await user.type(valueInput, 'nonNumericValue{Enter}');
316
+ expect(screen.queryByText('nonNumericValue')).not.toBeInTheDocument();
317
+ });
318
+ });
319
+ describe('when utilizing the "not in" operator', () => {
320
+ it('should allow numbers to be entered', async () => {
321
+ const user = userEvent.setup();
322
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
323
+ age: { $nin: [] },
324
+ }, setCriteria: setCriteriaMock }));
325
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
326
+ await user.click(valueInput);
327
+ await user.type(valueInput, '4{Enter}');
328
+ screen.getByRole('button', { name: '4' });
329
+ });
330
+ it('should not allow non-numeric values to be entered', async () => {
331
+ const user = userEvent.setup();
332
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
333
+ age: { $nin: [] },
334
+ }, setCriteria: setCriteriaMock }));
335
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
336
+ await user.click(valueInput);
337
+ await user.type(valueInput, 'nonNumericValue{Enter}');
338
+ expect(screen.queryByText('nonNumericValue')).not.toBeInTheDocument();
339
+ });
340
+ });
341
+ });
342
+ describe('when passed text fields', () => {
343
+ it('should render the field name', () => {
344
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
345
+ name: 'John Doe',
346
+ }, setCriteria: setCriteriaMock }));
347
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Name');
348
+ });
349
+ describe('when utilizing the "in" operator', () => {
350
+ it('should allow custom values to be entered', async () => {
351
+ const user = userEvent.setup();
352
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
353
+ name: { $in: [] },
354
+ }, setCriteria: setCriteriaMock }));
355
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
356
+ await user.click(valueInput);
357
+ await user.type(valueInput, 'customStatus{Enter}');
358
+ expect(screen.getByRole('button', { name: 'customStatus' })).toBeInTheDocument();
359
+ });
360
+ });
361
+ describe('when utilizing the "Not in" operator', () => {
362
+ it('should allow custom values to be entered', async () => {
363
+ const user = userEvent.setup();
364
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
365
+ name: { $nin: [] },
366
+ }, setCriteria: setCriteriaMock }));
367
+ const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
368
+ await user.click(valueInput);
369
+ await user.type(valueInput, 'customStatus{Enter}');
370
+ expect(screen.getByRole('button', { name: 'customStatus' })).toBeInTheDocument();
371
+ });
372
+ });
373
+ });
374
+ describe('when passed boolean fields', () => {
375
+ it('should render the field name', () => {
376
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
377
+ boolean: true,
378
+ }, setCriteria: setCriteriaMock }));
379
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Boolean');
380
+ });
381
+ ['Is', 'Is not', 'Is empty', 'Is not empty'].forEach((operator) => {
382
+ it(`should offer the ${operator} operator`, async () => {
383
+ const user = userEvent.setup();
384
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
385
+ boolean: true,
386
+ }, setCriteria: setCriteriaMock }));
387
+ await user.click(screen.getByRole('combobox', { name: /select operator/i }));
388
+ await screen.findByRole('option', { name: operator });
389
+ });
390
+ });
391
+ it('should display preloaded value', () => {
392
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
393
+ boolean: true,
394
+ }, setCriteria: setCriteriaMock }));
395
+ expect(screen.getByRole('combobox', { name: /select or enter a value/i })).toHaveValue('True');
396
+ });
397
+ ['True', 'False'].forEach((value) => {
398
+ it(`should offer the value ${value}`, async () => {
399
+ const user = userEvent.setup();
400
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
401
+ boolean: false,
402
+ }, setCriteria: setCriteriaMock }));
403
+ await user.click(screen.getByRole('combobox', { name: /select or enter a value/i }));
404
+ await screen.findByRole('option', { name: value });
405
+ });
406
+ });
407
+ });
408
+ describe('when deleting rules', () => {
409
+ it('should allow individual rules to be deleted if the delete rule button is pressed', async () => {
410
+ const user = userEvent.setup();
411
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
412
+ status: 'active',
413
+ }, setCriteria: setCriteriaMock }));
414
+ const propertySelectBox = screen.getByRole('combobox', { name: /select property/i });
415
+ const deleteButton = screen.getByRole('button', { name: /delete rule/i });
416
+ await user.click(deleteButton);
417
+ expect(propertySelectBox).not.toBeInTheDocument();
418
+ });
419
+ it('should remove an entire rule group if the delete group button is pressed', async () => {
420
+ const user = userEvent.setup();
421
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
422
+ $and: [{ $and: [{ status: 'active' }] }],
423
+ }, setCriteria: setCriteriaMock }));
424
+ const propertySelectBox = screen.getByRole('combobox', { name: /select property/i });
425
+ const deleteButton = screen.getByRole('button', { name: /delete group/i });
426
+ await user.click(deleteButton);
427
+ expect(propertySelectBox).not.toBeInTheDocument();
428
+ });
429
+ });
430
+ });
@@ -221,7 +221,10 @@ const ValueEditor = (props) => {
221
221
  })
222
222
  .filter((item) => item !== '');
223
223
  handleOnChange(uniqueSelections.length ? Array.from(new Set(uniqueSelections)) : '');
224
- }, isOptionEqualToValue: (option, value) => option.value === value.value, renderInput: (params) => (React.createElement(TextField, { label: params.label, ...params, size: "small" })), groupBy: (option) => isPresetValue(option.value) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sx: styles.input, readOnly: readOnly }));
224
+ }, isOptionEqualToValue: (option, value) => option.value === value.value, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, ...params, size: "small", inputProps: {
225
+ ...params.inputProps,
226
+ 'aria-label': 'Select or enter a value',
227
+ } })), groupBy: (option) => isPresetValue(option.value) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sx: styles.input, readOnly: readOnly }));
225
228
  }
226
229
  else {
227
230
  return (React.createElement(TextField, { inputRef: inputRef, value: ['null', 'notNull'].includes(operator) ? '' : value, disabled: disabled || ['null', 'notNull'].includes(operator), onChange: (e) => {
@@ -240,11 +243,14 @@ const ValueEditor = (props) => {
240
243
  const options = [{ label: 'True', value: true }, { label: 'False', value: false }, ...presetValues];
241
244
  return (React.createElement(Autocomplete, { options: options, value: options.find((opt) => opt.value === value) ?? value, onChange: (event, newValue) => {
242
245
  handleOnChange(newValue ? newValue.value : '');
243
- }, isOptionEqualToValue: (option, value) => option.value === value.value, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, label: params.label, ...params, size: "small" })), groupBy: (option) => isPresetValue(option.value) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sortBy: "NONE", sx: styles.input, readOnly: readOnly }));
246
+ }, isOptionEqualToValue: (option, value) => option.value === value.value, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, size: "small", ...params, inputProps: {
247
+ 'aria-label': 'Select or enter a value',
248
+ ...params.inputProps,
249
+ } })), groupBy: (option) => isPresetValue(option.value) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sortBy: "NONE", sx: styles.input, readOnly: readOnly }));
244
250
  }
245
251
  else {
246
- const isMultiple = inputType === 'array' || isMultipleOperator || values?.length;
247
- if (isMultiple) {
252
+ const isMultiple = inputType === 'array' || isMultipleOperator;
253
+ if (isMultiple || values?.length) {
248
254
  const options = [...values, ...presetValues];
249
255
  return (React.createElement(Autocomplete, { freeSolo: inputType !== 'array' && fieldData.valueEditorType !== 'select', multiple: isMultiple, options: options, value: isMultiple
250
256
  ? Array.isArray(value)
@@ -263,7 +269,11 @@ const ValueEditor = (props) => {
263
269
  }
264
270
  else {
265
271
  value =
266
- typeof newValue === 'string' ? newValue : newValue.value;
272
+ typeof newValue === 'string'
273
+ ? newValue
274
+ : !newValue
275
+ ? newValue
276
+ : newValue.value;
267
277
  }
268
278
  handleOnChange(value);
269
279
  }, onBlur: () => {
@@ -285,7 +295,10 @@ const ValueEditor = (props) => {
285
295
  }
286
296
  }, onInputChange: (event, newInputValue) => {
287
297
  setInputValue(newInputValue);
288
- }, inputValue: inputValue, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, label: params.label, ...params, size: "small" })), isOptionEqualToValue: (option, value) => option?.value === value.value, groupBy: (option) => isPresetValue(option.value) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sortBy: "NONE", sx: styles.input, readOnly: readOnly }));
298
+ }, inputValue: inputValue, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, ...params, size: "small", inputProps: {
299
+ ...params.inputProps,
300
+ 'aria-label': 'Select or enter a value',
301
+ } })), isOptionEqualToValue: (option, value) => typeof value === 'string' ? option?.value === value : option?.value === value.value, groupBy: (option) => isPresetValue(option.value) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sortBy: "NONE", sx: styles.input, readOnly: readOnly }));
289
302
  }
290
303
  else {
291
304
  return (React.createElement(TextField, { inputRef: inputRef, value: ['null', 'notNull'].includes(operator) ? '' : value, disabled: ['null', 'notNull'].includes(operator), onChange: (e) => handleOnChange(e.target.value), onClick: onClick, placeholder: "Value", size: "small", sx: styles.input, readOnly: readOnly }));
@@ -430,7 +430,7 @@ const RepeatableField = (props) => {
430
430
  hasCreateAction && (React.createElement(Button, { variant: "contained", sx: styles.addButton, onClick: addRow }, "Add"))),
431
431
  relatedObject && openDialog && (React.createElement(ActionDialog, { object: relatedObject, open: openDialog, apiServices: apiServices, onClose: () => setOpenDialog(false), instanceInput: dialogType === 'update' ? (relatedInstances.find((i) => i.id === selectedRow) ?? {}) : {}, handleSubmit: save,
432
432
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
433
- objectInputCommonProps: { apiServices }, action: relatedObject?.actions?.find((a) => a.id ===
433
+ objectInputCommonProps: { apiServices, setSnackbarError }, action: relatedObject?.actions?.find((a) => a.id ===
434
434
  (dialogType === 'create' ? '_create' : dialogType === 'update' ? '_update' : '_delete')), instanceId: selectedRow, queryAddresses: queryAddresses, user: user, associatedObject: instance.id && property.relatedPropertyId
435
435
  ? { instanceId: instance.id, propertyId: property.relatedPropertyId }
436
436
  : undefined, richTextEditor: richTextEditor })),
@@ -784,6 +784,7 @@ formComponents, allCriteriaInputs, instance, objectPropertyInputProps, associate
784
784
  item.autoSave = autoSave;
785
785
  item.apiServices = objectPropertyInputProps?.apiServices;
786
786
  item.user = objectPropertyInputProps?.user;
787
+ item.setSnackbarError = objectPropertyInputProps?.setSnackbarError;
787
788
  item.defaultPages = defaultPages;
788
789
  item.navigateTo = navigateTo;
789
790
  item.allCriteriaInputs = allCriteriaInputs;
@@ -1,6 +1,7 @@
1
1
  import { DateTimeFormatter } from '@js-joda/core';
2
2
  import { omit } from 'lodash';
3
3
  import React, { useEffect, useState } from 'react';
4
+ import { useFormContext } from '../../../../theme/hooks';
4
5
  import { InvalidDate, LocalDate, nativeJs } from '../../../../util';
5
6
  import { DatePicker, LocalizationProvider, TextField } from '../../../core';
6
7
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
@@ -29,6 +30,7 @@ const asMonthDayYearFormat = (date) => {
29
30
  const DatePickerSelect = (props) => {
30
31
  const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, onChange, additionalProps, } = props;
31
32
  const [value, setValue] = useState(asCalendarDate(defaultValue));
33
+ const { onAutosave } = useFormContext();
32
34
  useEffect(() => {
33
35
  setValue(asCalendarDate(defaultValue));
34
36
  }, [defaultValue]);
@@ -36,8 +38,19 @@ const DatePickerSelect = (props) => {
36
38
  setValue(date);
37
39
  onChange && onChange(property.id, date, property);
38
40
  };
41
+ const handleAccept = async () => {
42
+ // Trigger autosave when date is accepted (picker closes after selection)
43
+ if (onAutosave) {
44
+ try {
45
+ await onAutosave(id);
46
+ }
47
+ catch (error) {
48
+ console.error('Autosave failed:', error);
49
+ }
50
+ }
51
+ };
39
52
  return readOnly ? (React.createElement(InputFieldComponent, { ...{ ...props, defaultValue: asMonthDayYearFormat(value) } })) : (React.createElement(LocalizationProvider, null,
40
- React.createElement(DatePicker, { value: value, onChange: handleChange, inputFormat: "MM/dd/yyyy", renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
53
+ React.createElement(DatePicker, { value: value, onChange: handleChange, onAccept: handleAccept, inputFormat: "MM/dd/yyyy", renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
41
54
  // merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
42
55
  inputProps: {
43
56
  ...params.inputProps,
@@ -1,6 +1,7 @@
1
1
  import { LocalDate, LocalDateTime, LocalTime, nativeJs } from '@js-joda/core';
2
2
  import { omit } from 'lodash';
3
3
  import React, { useEffect, useState } from 'react';
4
+ import { useFormContext } from '../../../../theme/hooks';
4
5
  import { InvalidDate } from '../../../../util';
5
6
  import { DateTimePicker, LocalizationProvider, TextField } from '../../../core';
6
7
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
@@ -30,6 +31,7 @@ const formatDateTime = (date) => {
30
31
  const DateTimePickerSelect = (props) => {
31
32
  const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, additionalProps } = props;
32
33
  const [value, setValue] = useState(asCalendarDate(defaultValue));
34
+ const { onAutosave } = useFormContext();
33
35
  useEffect(() => {
34
36
  setValue(asCalendarDate(defaultValue));
35
37
  }, [defaultValue]);
@@ -43,8 +45,19 @@ const DateTimePickerSelect = (props) => {
43
45
  setValue(date);
44
46
  props.onChange && props.onChange(property.id, date, property);
45
47
  };
48
+ const handleAccept = async () => {
49
+ // Trigger autosave when date/time is accepted (picker closes after selection)
50
+ if (onAutosave) {
51
+ try {
52
+ await onAutosave(id);
53
+ }
54
+ catch (error) {
55
+ console.error('Autosave failed:', error);
56
+ }
57
+ }
58
+ };
46
59
  return readOnly ? (React.createElement(InputFieldComponent, { ...{ ...props, defaultValue: formatDateTime(value) } })) : (React.createElement(LocalizationProvider, null,
47
- React.createElement(DateTimePicker, { value: value, onChange: handleChange, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
60
+ React.createElement(DateTimePicker, { value: value, onChange: handleChange, onAccept: handleAccept, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
48
61
  // merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
49
62
  inputProps: {
50
63
  ...params.inputProps,
@@ -3,11 +3,13 @@ import { TimePicker } from '@mui/x-date-pickers';
3
3
  import { isUndefined, omit, padStart } from 'lodash';
4
4
  import { DateTime } from 'luxon';
5
5
  import React, { useEffect, useState } from 'react';
6
+ import { useFormContext } from '../../../../theme/hooks';
6
7
  import { InvalidDate } from '../../../../util';
7
8
  import { LocalizationProvider, TextField } from '../../../core';
8
9
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
9
10
  const TimePickerSelect = (props) => {
10
11
  const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, placeholder, additionalProps, } = props;
12
+ const { onAutosave } = useFormContext();
11
13
  const values = defaultValue ? defaultValue.split(':') : undefined;
12
14
  const hour = values ? parseInt(values[0]) : undefined;
13
15
  const minute = values ? parseInt(values[1]) : undefined;
@@ -41,11 +43,22 @@ const TimePickerSelect = (props) => {
41
43
  props.onChange && props.onChange(property.id, date, property);
42
44
  }
43
45
  };
46
+ const handleAccept = async () => {
47
+ // Trigger autosave when time is accepted (picker closes after selection)
48
+ if (onAutosave) {
49
+ try {
50
+ await onAutosave(id);
51
+ }
52
+ catch (error) {
53
+ console.error('Autosave failed:', error);
54
+ }
55
+ }
56
+ };
44
57
  return readOnly ? (React.createElement(InputFieldComponent, { ...{
45
58
  ...props,
46
59
  defaultValue: value instanceof LocalDateTime ? DateTime.fromISO(value.toString()).toFormat('hh:mm a') : '',
47
60
  } })) : (React.createElement(LocalizationProvider, null,
48
- React.createElement(TimePicker, { value: value, onChange: handleChange, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium', placeholder: placeholder,
61
+ React.createElement(TimePicker, { value: value, onChange: handleChange, onAccept: handleAccept, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium', placeholder: placeholder,
49
62
  // merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
50
63
  inputProps: {
51
64
  ...params.inputProps,
@@ -16,7 +16,8 @@ export type FormRendererProps = BaseProps & {
16
16
  form: EvokeForm;
17
17
  title?: string | React.ReactNode;
18
18
  instance?: ObjectInstance | Document;
19
- onChange: (id: string, value: unknown) => void;
19
+ onChange: (id: string, value: unknown) => void | Promise<void>;
20
+ onAutosave?: (fieldId: string) => void | Promise<void>;
20
21
  associatedObject?: {
21
22
  instanceId?: string;
22
23
  propertyId?: string;