@evoke-platform/ui-components 1.10.0-testing.1 → 1.10.0-testing.11
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 +1 -1
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +430 -0
- package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
- package/dist/published/components/custom/Form/utils.js +1 -0
- package/dist/published/components/custom/FormV2/FormRenderer.js +13 -3
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +0 -2
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +10 -5
- package/dist/published/components/custom/FormV2/components/Footer.js +1 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +0 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +31 -27
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +1 -2
- package/dist/published/components/custom/FormV2/components/Header.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Header.js +5 -2
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +22 -17
- package/dist/published/components/custom/FormV2/components/utils.js +2 -7
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +432 -4
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +202 -12
- package/dist/published/components/custom/FormV2/tests/test-data.js +2 -0
- package/dist/published/stories/FormRendererContainer.stories.d.ts +0 -10
- package/dist/published/stories/FormRendererContainer.stories.js +7 -3
- package/dist/published/stories/FormRendererData.js +3 -43
- package/package.json +3 -1
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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, {
|
|
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,
|
|
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
|
|
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'
|
|
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,
|
|
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 }));
|
|
@@ -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,6 @@
|
|
|
1
1
|
import { useObject } from '@evoke-platform/context';
|
|
2
2
|
import { isEmpty, isEqual, omit } from 'lodash';
|
|
3
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useForm } from 'react-hook-form';
|
|
5
5
|
import { useWidgetSize } from '../../../theme';
|
|
6
6
|
import { Box } from '../../layout';
|
|
@@ -12,7 +12,7 @@ import { assignIdsToSectionsAndRichText, convertDocToParameters, convertProperti
|
|
|
12
12
|
import { handleValidation } from './components/ValidationFiles/Validation';
|
|
13
13
|
import ValidationErrors from './components/ValidationFiles/ValidationErrors';
|
|
14
14
|
const FormRendererInternal = (props) => {
|
|
15
|
-
const { onSubmit, onDiscardChanges, onSubmitError, value, fieldHeight, richTextEditor, form, instance, onChange, associatedObject, renderHeader, renderBody, renderFooter, } = props;
|
|
15
|
+
const { onSubmit, onDiscardChanges, onSubmitError: onSubmitErrorOverride, value, fieldHeight, richTextEditor, form, instance, onChange, associatedObject, renderHeader, renderBody, renderFooter, } = props;
|
|
16
16
|
const { entries, name: title, objectId, actionId, display } = form;
|
|
17
17
|
const { register, unregister, setValue, reset, handleSubmit, formState: { errors, isSubmitted }, getValues, } = useForm({
|
|
18
18
|
defaultValues: value,
|
|
@@ -32,6 +32,7 @@ const FormRendererInternal = (props) => {
|
|
|
32
32
|
const [isInitializing, setIsInitializing] = useState(true);
|
|
33
33
|
const [parameters, setParameters] = useState();
|
|
34
34
|
const objectStore = useObject(objectId);
|
|
35
|
+
const validationErrorsRef = useRef(null);
|
|
35
36
|
const updateFetchedOptions = (newData) => {
|
|
36
37
|
setFetchedOptions((prev) => ({
|
|
37
38
|
...prev,
|
|
@@ -134,9 +135,17 @@ const FormRendererInternal = (props) => {
|
|
|
134
135
|
unregister(fieldId);
|
|
135
136
|
}
|
|
136
137
|
};
|
|
138
|
+
const onSubmitError = (errors) => {
|
|
139
|
+
if (onSubmitErrorOverride) {
|
|
140
|
+
onSubmitErrorOverride(errors);
|
|
141
|
+
}
|
|
142
|
+
else if (validationErrorsRef.current) {
|
|
143
|
+
validationErrorsRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
137
146
|
async function unregisterHiddenFieldsAndSubmit() {
|
|
138
147
|
unregisterHiddenFields(entries ?? []);
|
|
139
|
-
await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError
|
|
148
|
+
await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError(errors))();
|
|
140
149
|
}
|
|
141
150
|
const headerProps = {
|
|
142
151
|
title,
|
|
@@ -148,6 +157,7 @@ const FormRendererInternal = (props) => {
|
|
|
148
157
|
shouldShowValidationErrors: isSubmitted,
|
|
149
158
|
form,
|
|
150
159
|
action,
|
|
160
|
+
validationErrorsRef: validationErrorsRef,
|
|
151
161
|
};
|
|
152
162
|
const footerProps = {
|
|
153
163
|
onSubmit: unregisterHiddenFieldsAndSubmit,
|
|
@@ -24,8 +24,6 @@ export type FormRendererContainerProps = BaseProps & {
|
|
|
24
24
|
fieldHeight?: 'small' | 'medium';
|
|
25
25
|
};
|
|
26
26
|
actionId?: string;
|
|
27
|
-
stickyFooter?: boolean;
|
|
28
|
-
hideButtons?: boolean;
|
|
29
27
|
objectId: string;
|
|
30
28
|
richTextEditor?: ComponentType<SimpleEditorProps>;
|
|
31
29
|
onSubmit?: (submission: Record<string, unknown>, defaultSubmitHandler: (submission: Record<string, unknown>) => Promise<void>) => Promise<void>;
|
|
@@ -59,6 +59,10 @@ function FormRendererContainer(props) {
|
|
|
59
59
|
const action = object?.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
60
60
|
if (action && (instanceId || action.type === 'create')) {
|
|
61
61
|
setAction(action);
|
|
62
|
+
// Clear error if action is found after being missing
|
|
63
|
+
// TODO: This entire effect should take place after form is fetched to avoid an error flickering
|
|
64
|
+
// That is, this effect should be merged with the one below that fetches the form
|
|
65
|
+
setError((prevError) => prevError === 'Action could not be found' ? undefined : prevError);
|
|
62
66
|
}
|
|
63
67
|
else {
|
|
64
68
|
setError('Action could not be found');
|
|
@@ -99,7 +103,8 @@ function FormRendererContainer(props) {
|
|
|
99
103
|
apiServices
|
|
100
104
|
.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
|
|
101
105
|
.then((evokeForm) => {
|
|
102
|
-
|
|
106
|
+
// If an actionId is provided, ensure it matches the form's actionId
|
|
107
|
+
if (!actionId || evokeForm?.actionId === actionId) {
|
|
103
108
|
const form = evokeForm;
|
|
104
109
|
setForm(form);
|
|
105
110
|
}
|
|
@@ -245,8 +250,8 @@ function FormRendererContainer(props) {
|
|
|
245
250
|
else if (action?.type === 'create') {
|
|
246
251
|
const response = await apiServices.post(getPrefixedUrl(`/objects/${form.objectId}/instances/actions`), {
|
|
247
252
|
actionId: form.actionId,
|
|
248
|
-
input:
|
|
249
|
-
?.filter((property) =>
|
|
253
|
+
input: omit(submission, sanitizedObject?.properties
|
|
254
|
+
?.filter((property) => property.formula || property.type === 'collection')
|
|
250
255
|
.map((property) => property.id) ?? []),
|
|
251
256
|
});
|
|
252
257
|
if (response) {
|
|
@@ -256,8 +261,8 @@ function FormRendererContainer(props) {
|
|
|
256
261
|
else if (instanceId && action) {
|
|
257
262
|
const response = await objectStore.instanceAction(instanceId, {
|
|
258
263
|
actionId: action.id,
|
|
259
|
-
input:
|
|
260
|
-
?.filter((property) =>
|
|
264
|
+
input: omit(submission, sanitizedObject?.properties
|
|
265
|
+
?.filter((property) => property.formula || property.type === 'collection')
|
|
261
266
|
.map((property) => property.id) ?? []),
|
|
262
267
|
});
|
|
263
268
|
if (sanitizedObject && instance) {
|
|
@@ -87,7 +87,7 @@ export const ActionDialog = (props) => {
|
|
|
87
87
|
borderBottom: action.type === 'delete' ? undefined : '1px solid #e9ecef',
|
|
88
88
|
} },
|
|
89
89
|
action && hasAccess && !loading ? action?.name : '',
|
|
90
|
-
React.createElement(IconButton, { sx: styles.closeIcon, onClick: onClose },
|
|
90
|
+
React.createElement(IconButton, { sx: styles.closeIcon, onClick: onClose, "aria-label": "Close" },
|
|
91
91
|
React.createElement(Close, { fontSize: "small" })),
|
|
92
92
|
formHeaderProps.hasAccordions && React.createElement(AccordionActions, { ...formHeaderProps })));
|
|
93
93
|
}, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: {
|
|
@@ -6,9 +6,6 @@ export type ObjectPropertyInputProps = {
|
|
|
6
6
|
criteria?: object;
|
|
7
7
|
viewLayout?: ViewLayoutEntityReference;
|
|
8
8
|
entry: InputField | InputParameterReference | ReadonlyField;
|
|
9
|
-
createActionId?: string;
|
|
10
|
-
updateActionId?: string;
|
|
11
|
-
deleteActionId?: string;
|
|
12
9
|
};
|
|
13
10
|
declare const RepeatableField: (props: ObjectPropertyInputProps) => React.JSX.Element;
|
|
14
11
|
export default RepeatableField;
|