@openmrs/esm-form-engine-lib 2.1.0-pre.1466 → 2.1.0-pre.1476
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/75e0dfeda364f646/75e0dfeda364f646.gz +0 -0
- package/__mocks__/forms/rfe-forms/multi-select-form.json +20 -0
- package/b211b244de3a9350/b211b244de3a9350.gz +0 -0
- package/cacdacff9a5c84a8/cacdacff9a5c84a8.gz +0 -0
- package/cbf7151ea0b21ed7/cbf7151ea0b21ed7.gz +0 -0
- package/package.json +1 -1
- package/src/adapters/program-state-adapter.test.ts +415 -0
- package/src/components/inputs/multi-select/multi-select.component.tsx +26 -20
- package/src/components/inputs/multi-select/multi-select.test.tsx +15 -5
- package/src/form-engine.test.tsx +13 -14
- package/src/transformers/default-schema-transformer.test.ts +56 -0
- package/src/transformers/default-schema-transformer.ts +20 -3
- package/src/types/schema.ts +10 -0
- package/src/utils/test-utils.ts +7 -2
Binary file
|
@@ -71,6 +71,26 @@
|
|
71
71
|
},
|
72
72
|
"inlineRendering": null,
|
73
73
|
"isHidden": false
|
74
|
+
},
|
75
|
+
{
|
76
|
+
"label": "Checkbox searchable",
|
77
|
+
"id": "checkboxSearchable",
|
78
|
+
"questionOptions": {
|
79
|
+
"rendering": "multiCheckbox",
|
80
|
+
"concept": "d49e3e6-55df-5096-93ca-59edadb74b3f",
|
81
|
+
"answers": [
|
82
|
+
{
|
83
|
+
"concept": "8b715fed-97f6-4e38-8f6a-c167a42f8923",
|
84
|
+
"label": "Option 1"
|
85
|
+
},
|
86
|
+
{
|
87
|
+
"concept": "a899e0ac-1350-11df-a1f1-0026b9348838",
|
88
|
+
"label": "Option 2"
|
89
|
+
}
|
90
|
+
]
|
91
|
+
},
|
92
|
+
"type": "obs",
|
93
|
+
"validators": []
|
74
94
|
}
|
75
95
|
]
|
76
96
|
}
|
Binary file
|
Binary file
|
Binary file
|
package/package.json
CHANGED
@@ -0,0 +1,415 @@
|
|
1
|
+
import { type FormContextProps } from '../provider/form-provider';
|
2
|
+
import { type FormField } from '../types';
|
3
|
+
import { ProgramStateAdapter } from './program-state-adapter';
|
4
|
+
|
5
|
+
const formContext = {
|
6
|
+
methods: null,
|
7
|
+
workspaceLayout: 'maximized',
|
8
|
+
isSubmitting: false,
|
9
|
+
patient: {
|
10
|
+
id: '833db896-c1f0-11eb-8529-0242ac130003',
|
11
|
+
},
|
12
|
+
formJson: null,
|
13
|
+
visit: null,
|
14
|
+
sessionMode: 'enter',
|
15
|
+
sessionDate: new Date(),
|
16
|
+
location: {
|
17
|
+
uuid: '41e6e516-c1f0-11eb-8529-0242ac130003',
|
18
|
+
},
|
19
|
+
currentProvider: null,
|
20
|
+
layoutType: 'small-desktop',
|
21
|
+
domainObjectValue: {
|
22
|
+
uuid: '873455da-3ec4-453c-b565-7c1fe35426be',
|
23
|
+
obs: [],
|
24
|
+
},
|
25
|
+
previousDomainObjectValue: null,
|
26
|
+
processor: null,
|
27
|
+
formFields: [],
|
28
|
+
formFieldAdapters: null,
|
29
|
+
formFieldValidators: null,
|
30
|
+
customDependencies: {
|
31
|
+
patientPrograms: [],
|
32
|
+
},
|
33
|
+
getFormField: jest.fn(),
|
34
|
+
addFormField: jest.fn(),
|
35
|
+
updateFormField: jest.fn(),
|
36
|
+
removeFormField: () => {},
|
37
|
+
addInvalidField: jest.fn(),
|
38
|
+
removeInvalidField: jest.fn(),
|
39
|
+
setInvalidFields: jest.fn(),
|
40
|
+
setForm: jest.fn(),
|
41
|
+
} as FormContextProps;
|
42
|
+
|
43
|
+
const field = {
|
44
|
+
label: 'HIV Enrollment Initial State',
|
45
|
+
type: 'programState',
|
46
|
+
required: false,
|
47
|
+
id: 'hivEnrollmentInitialState',
|
48
|
+
questionOptions: {
|
49
|
+
rendering: 'select',
|
50
|
+
answers: [
|
51
|
+
{
|
52
|
+
value: '7293cb90-c93f-4386-b32f-e8cfc633dc3e',
|
53
|
+
label: 'Example Option 1',
|
54
|
+
},
|
55
|
+
{
|
56
|
+
value: 'c26a8cc7-fb07-4b2f-bdb0-730db9ce0020',
|
57
|
+
label: 'Example Option 2',
|
58
|
+
},
|
59
|
+
{
|
60
|
+
value: '29a513f0-2810-4356-98f5-42b12f7013a5',
|
61
|
+
label: 'Example Option 3',
|
62
|
+
},
|
63
|
+
{
|
64
|
+
value: '7c0a5653-313f-4564-b9cf-d59adf1173dc',
|
65
|
+
label: 'On Antiretrovirals Treatment',
|
66
|
+
},
|
67
|
+
],
|
68
|
+
programUuid: '64f950e6-1b07-4ac0-8e7e-f3e148f3463f',
|
69
|
+
workflowUuid: '70921392-4e3e-5465-978d-45b68b7def5f',
|
70
|
+
},
|
71
|
+
meta: {
|
72
|
+
submission: {},
|
73
|
+
},
|
74
|
+
} as FormField;
|
75
|
+
|
76
|
+
const patientPrograms = [
|
77
|
+
{
|
78
|
+
uuid: 'c0dd89a7-62d5-40ed-850a-d3b1709ea7f2',
|
79
|
+
display: 'HIV Care and Treatment',
|
80
|
+
program: {
|
81
|
+
uuid: '64f950e6-1b07-4ac0-8e7e-f3e148f3463f',
|
82
|
+
name: 'HIV Care and Treatment',
|
83
|
+
allWorkflows: [
|
84
|
+
{
|
85
|
+
uuid: '70921392-4e3e-5465-978d-45b68b7def5f',
|
86
|
+
concept: {
|
87
|
+
uuid: '7dc379f6-1725-11ed-861d-0242ac120002',
|
88
|
+
display: 'HIV treatment status',
|
89
|
+
links: [
|
90
|
+
{
|
91
|
+
rel: 'self',
|
92
|
+
uri: 'http://dev3.openmrs.org/openmrs/ws/rest/v1/concept/7dc379f6-1725-11ed-861d-0242ac120002',
|
93
|
+
resourceAlias: 'concept',
|
94
|
+
},
|
95
|
+
],
|
96
|
+
},
|
97
|
+
description: null,
|
98
|
+
retired: false,
|
99
|
+
states: [
|
100
|
+
{
|
101
|
+
uuid: '7293cb90-c93f-4386-b32f-e8cfc633dc3e',
|
102
|
+
description: null,
|
103
|
+
retired: false,
|
104
|
+
concept: {
|
105
|
+
uuid: 'acc6f157-c9a5-4690-b814-e653cbf80b4c',
|
106
|
+
display: 'Example Option 1',
|
107
|
+
name: {
|
108
|
+
display: 'Example Option 1',
|
109
|
+
uuid: '51d0d6c4-0361-479e-aec4-b40ee82fb23c',
|
110
|
+
name: 'Example Option 1',
|
111
|
+
locale: 'en',
|
112
|
+
localePreferred: true,
|
113
|
+
conceptNameType: 'FULLY_SPECIFIED',
|
114
|
+
links: [],
|
115
|
+
resourceVersion: '1.9',
|
116
|
+
},
|
117
|
+
datatype: {
|
118
|
+
uuid: '8d4a4c94-c2cc-11de-8d13-0010c6dffd0f',
|
119
|
+
display: 'N/A',
|
120
|
+
links: [],
|
121
|
+
},
|
122
|
+
conceptClass: {
|
123
|
+
uuid: '8d492774-c2cc-11de-8d13-0010c6dffd0f',
|
124
|
+
display: 'Misc',
|
125
|
+
links: [],
|
126
|
+
},
|
127
|
+
set: false,
|
128
|
+
version: null,
|
129
|
+
retired: false,
|
130
|
+
names: [
|
131
|
+
{
|
132
|
+
uuid: '51d0d6c4-0361-479e-aec4-b40ee82fb23c',
|
133
|
+
display: 'Example Option 1',
|
134
|
+
links: [],
|
135
|
+
},
|
136
|
+
],
|
137
|
+
descriptions: [],
|
138
|
+
mappings: [],
|
139
|
+
answers: [],
|
140
|
+
setMembers: [],
|
141
|
+
attributes: [],
|
142
|
+
links: [],
|
143
|
+
resourceVersion: '2.0',
|
144
|
+
},
|
145
|
+
links: [
|
146
|
+
{
|
147
|
+
rel: 'self',
|
148
|
+
uri: 'http://dev3.openmrs.org/openmrs/ws/rest/v1/workflow/70921392-4e3e-5465-978d-45b68b7def5f/state/7293cb90-c93f-4386-b32f-e8cfc633dc3e',
|
149
|
+
resourceAlias: 'state',
|
150
|
+
},
|
151
|
+
{
|
152
|
+
rel: 'full',
|
153
|
+
uri: 'http://dev3.openmrs.org/openmrs/ws/rest/v1/workflow/70921392-4e3e-5465-978d-45b68b7def5f/state/7293cb90-c93f-4386-b32f-e8cfc633dc3e?v=full',
|
154
|
+
resourceAlias: 'state',
|
155
|
+
},
|
156
|
+
],
|
157
|
+
resourceVersion: '1.8',
|
158
|
+
},
|
159
|
+
{
|
160
|
+
uuid: 'c26a8cc7-fb07-4b2f-bdb0-730db9ce0020',
|
161
|
+
description: null,
|
162
|
+
retired: false,
|
163
|
+
concept: {
|
164
|
+
uuid: '4f2f8cd9-e57c-4bcd-822f-0dfabb684bc1',
|
165
|
+
display: 'Example Option 2',
|
166
|
+
name: {
|
167
|
+
display: 'Example Option 2',
|
168
|
+
uuid: 'd0934337-68bd-47ff-83f7-075c8d15f31c',
|
169
|
+
name: 'Example Option 2',
|
170
|
+
locale: 'en',
|
171
|
+
localePreferred: true,
|
172
|
+
conceptNameType: 'FULLY_SPECIFIED',
|
173
|
+
links: [],
|
174
|
+
resourceVersion: '1.9',
|
175
|
+
},
|
176
|
+
datatype: {
|
177
|
+
uuid: '8d4a4c94-c2cc-11de-8d13-0010c6dffd0f',
|
178
|
+
display: 'N/A',
|
179
|
+
links: [],
|
180
|
+
},
|
181
|
+
conceptClass: {
|
182
|
+
uuid: '8d492774-c2cc-11de-8d13-0010c6dffd0f',
|
183
|
+
display: 'Misc',
|
184
|
+
links: [],
|
185
|
+
},
|
186
|
+
set: false,
|
187
|
+
version: null,
|
188
|
+
retired: false,
|
189
|
+
names: [
|
190
|
+
{
|
191
|
+
uuid: 'd0934337-68bd-47ff-83f7-075c8d15f31c',
|
192
|
+
display: 'Example Option 2',
|
193
|
+
links: [],
|
194
|
+
},
|
195
|
+
],
|
196
|
+
descriptions: [],
|
197
|
+
mappings: [],
|
198
|
+
answers: [],
|
199
|
+
setMembers: [],
|
200
|
+
attributes: [],
|
201
|
+
links: [],
|
202
|
+
resourceVersion: '2.0',
|
203
|
+
},
|
204
|
+
links: [],
|
205
|
+
resourceVersion: '1.8',
|
206
|
+
},
|
207
|
+
{
|
208
|
+
uuid: '7c0a5653-313f-4564-b9cf-d59adf1173dc',
|
209
|
+
description: null,
|
210
|
+
retired: false,
|
211
|
+
concept: {
|
212
|
+
uuid: '7dc37bb8-1725-11ed-861d-0242ac120002',
|
213
|
+
display: 'On Antiretrovirals Treatment',
|
214
|
+
name: {
|
215
|
+
display: 'On Antiretrovirals Treatment',
|
216
|
+
uuid: 'c90ed5f3-4c4d-303b-b8bd-d6b5d21c0f81',
|
217
|
+
name: 'On Antiretrovirals Treatment',
|
218
|
+
locale: 'en',
|
219
|
+
localePreferred: true,
|
220
|
+
conceptNameType: 'FULLY_SPECIFIED',
|
221
|
+
links: [],
|
222
|
+
resourceVersion: '1.9',
|
223
|
+
},
|
224
|
+
datatype: {
|
225
|
+
uuid: '8d4a4c94-c2cc-11de-8d13-0010c6dffd0f',
|
226
|
+
display: 'N/A',
|
227
|
+
links: [],
|
228
|
+
},
|
229
|
+
conceptClass: {
|
230
|
+
uuid: '8d492774-c2cc-11de-8d13-0010c6dffd0f',
|
231
|
+
display: 'Misc',
|
232
|
+
links: [],
|
233
|
+
},
|
234
|
+
set: false,
|
235
|
+
version: null,
|
236
|
+
retired: false,
|
237
|
+
names: [
|
238
|
+
{
|
239
|
+
uuid: 'c90ed5f3-4c4d-303b-b8bd-d6b5d21c0f81',
|
240
|
+
display: 'On Antiretrovirals Treatment',
|
241
|
+
links: [],
|
242
|
+
},
|
243
|
+
{
|
244
|
+
uuid: '3d61609a-30e1-3dd1-83a0-21310b06c388',
|
245
|
+
display: 'On Antiretrovirals',
|
246
|
+
links: [],
|
247
|
+
},
|
248
|
+
],
|
249
|
+
descriptions: [
|
250
|
+
{
|
251
|
+
uuid: '07fc6455-9ffa-4848-a4bc-e58ffb91503a',
|
252
|
+
display: 'On Antiretrovirals Treatment program workflow state',
|
253
|
+
links: [],
|
254
|
+
},
|
255
|
+
],
|
256
|
+
mappings: [
|
257
|
+
{
|
258
|
+
uuid: 'fffc1802-f832-49ef-bfaa-0ffe4bc7db0f',
|
259
|
+
display: 'SNOMED MVP: OATT',
|
260
|
+
links: [],
|
261
|
+
},
|
262
|
+
],
|
263
|
+
answers: [],
|
264
|
+
setMembers: [],
|
265
|
+
attributes: [],
|
266
|
+
links: [],
|
267
|
+
resourceVersion: '2.0',
|
268
|
+
},
|
269
|
+
links: [],
|
270
|
+
resourceVersion: '1.8',
|
271
|
+
},
|
272
|
+
{
|
273
|
+
uuid: '29a513f0-2810-4356-98f5-42b12f7013a5',
|
274
|
+
description: null,
|
275
|
+
retired: false,
|
276
|
+
concept: {
|
277
|
+
uuid: 'e1ad9977-c106-4116-9f23-45258b7e306b',
|
278
|
+
display: 'Example Option 3',
|
279
|
+
name: {
|
280
|
+
display: 'Example Option 3',
|
281
|
+
uuid: '988af00b-6243-4fc2-9d81-718bc9bbe743',
|
282
|
+
name: 'Example Option 3',
|
283
|
+
locale: 'en',
|
284
|
+
localePreferred: true,
|
285
|
+
conceptNameType: 'FULLY_SPECIFIED',
|
286
|
+
links: [],
|
287
|
+
resourceVersion: '1.9',
|
288
|
+
},
|
289
|
+
datatype: {
|
290
|
+
uuid: '8d4a4c94-c2cc-11de-8d13-0010c6dffd0f',
|
291
|
+
display: 'N/A',
|
292
|
+
links: [],
|
293
|
+
},
|
294
|
+
conceptClass: {
|
295
|
+
uuid: '8d492774-c2cc-11de-8d13-0010c6dffd0f',
|
296
|
+
display: 'Misc',
|
297
|
+
links: [],
|
298
|
+
},
|
299
|
+
set: false,
|
300
|
+
version: null,
|
301
|
+
retired: false,
|
302
|
+
names: [
|
303
|
+
{
|
304
|
+
uuid: '988af00b-6243-4fc2-9d81-718bc9bbe743',
|
305
|
+
display: 'Example Option 3',
|
306
|
+
links: [
|
307
|
+
{
|
308
|
+
rel: 'self',
|
309
|
+
uri: 'http://dev3.openmrs.org/openmrs/ws/rest/v1/concept/e1ad9977-c106-4116-9f23-45258b7e306b/name/988af00b-6243-4fc2-9d81-718bc9bbe743',
|
310
|
+
resourceAlias: 'name',
|
311
|
+
},
|
312
|
+
],
|
313
|
+
},
|
314
|
+
],
|
315
|
+
descriptions: [],
|
316
|
+
mappings: [],
|
317
|
+
answers: [],
|
318
|
+
setMembers: [],
|
319
|
+
attributes: [],
|
320
|
+
links: [],
|
321
|
+
resourceVersion: '2.0',
|
322
|
+
},
|
323
|
+
links: [],
|
324
|
+
resourceVersion: '1.8',
|
325
|
+
},
|
326
|
+
],
|
327
|
+
links: [],
|
328
|
+
resourceVersion: '1.8',
|
329
|
+
},
|
330
|
+
],
|
331
|
+
},
|
332
|
+
dateEnrolled: '2024-09-23T09:31:51.000+0000',
|
333
|
+
dateCompleted: null,
|
334
|
+
location: null,
|
335
|
+
states: [
|
336
|
+
{
|
337
|
+
startDate: '2024-09-23T00:00:00.000+0000',
|
338
|
+
endDate: null,
|
339
|
+
state: {
|
340
|
+
uuid: '7293cb90-c93f-4386-b32f-e8cfc633dc3e',
|
341
|
+
name: null,
|
342
|
+
retired: false,
|
343
|
+
concept: {
|
344
|
+
uuid: 'acc6f157-c9a5-4690-b814-e653cbf80b4c',
|
345
|
+
},
|
346
|
+
programWorkflow: {
|
347
|
+
uuid: '70921392-4e3e-5465-978d-45b68b7def5f',
|
348
|
+
},
|
349
|
+
},
|
350
|
+
},
|
351
|
+
],
|
352
|
+
},
|
353
|
+
];
|
354
|
+
|
355
|
+
describe('ProgramStateAdapter', () => {
|
356
|
+
// new submission (enter mode)
|
357
|
+
it('should handle submission for a program state', () => {
|
358
|
+
const value = '7293cb90-c93f-4386-b32f-e8cfc633dc3e';
|
359
|
+
ProgramStateAdapter.transformFieldValue(field, value, formContext);
|
360
|
+
expect(field.meta.submission.newValue).toEqual({
|
361
|
+
state: value,
|
362
|
+
startDate: expect.any(String),
|
363
|
+
});
|
364
|
+
});
|
365
|
+
|
366
|
+
it('should return null if the new value is the same as the previous value', () => {
|
367
|
+
field.meta.previousValue = { uuid: '7293cb90-c93f-4386-b32f-e8cfc633dc3e' };
|
368
|
+
const value = '7293cb90-c93f-4386-b32f-e8cfc633dc3e';
|
369
|
+
const result = ProgramStateAdapter.transformFieldValue(field, value, formContext);
|
370
|
+
expect(result).toBeNull();
|
371
|
+
expect(field.meta.submission.newValue).toBeNull();
|
372
|
+
});
|
373
|
+
|
374
|
+
it('should return null if the new value is empty or null', () => {
|
375
|
+
const value = null;
|
376
|
+
const result = ProgramStateAdapter.transformFieldValue(field, value, formContext);
|
377
|
+
expect(result).toBeNull();
|
378
|
+
expect(field.meta.submission.newValue).toBeNull();
|
379
|
+
});
|
380
|
+
|
381
|
+
it('should get initial value for the program state', async () => {
|
382
|
+
formContext.customDependencies.patientPrograms.push(...patientPrograms);
|
383
|
+
const program = await ProgramStateAdapter.getInitialValue(field, null, formContext);
|
384
|
+
expect(program).toEqual('7293cb90-c93f-4386-b32f-e8cfc633dc3e');
|
385
|
+
});
|
386
|
+
|
387
|
+
it('should return null if no active state is found for the patient program', async () => {
|
388
|
+
formContext.customDependencies.patientPrograms = [
|
389
|
+
{
|
390
|
+
...patientPrograms[0],
|
391
|
+
states: [],
|
392
|
+
},
|
393
|
+
];
|
394
|
+
const p = await ProgramStateAdapter.getInitialValue(field, null, formContext);
|
395
|
+
expect(p).toBeNull();
|
396
|
+
});
|
397
|
+
|
398
|
+
it('should return null if no patient program matches the programUuid', async () => {
|
399
|
+
const fieldWithDifferentProgramUuid = {
|
400
|
+
...field,
|
401
|
+
questionOptions: { ...field.questionOptions, programUuid: 'non-existing-uuid' },
|
402
|
+
};
|
403
|
+
const program = await ProgramStateAdapter.getInitialValue(fieldWithDifferentProgramUuid, null, formContext);
|
404
|
+
expect(program).toBeNull();
|
405
|
+
});
|
406
|
+
|
407
|
+
it('should return null for getPreviousValue', async () => {
|
408
|
+
const previousValue = await ProgramStateAdapter.getPreviousValue(field, null, formContext);
|
409
|
+
expect(previousValue).toBeNull();
|
410
|
+
});
|
411
|
+
|
412
|
+
it('should execute tearDown without issues', () => {
|
413
|
+
expect(() => ProgramStateAdapter.tearDown()).not.toThrow();
|
414
|
+
});
|
415
|
+
});
|
@@ -42,6 +42,11 @@ const MultiSelect: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
|
|
42
42
|
setFieldValue(value);
|
43
43
|
};
|
44
44
|
|
45
|
+
const isSearchable = useMemo(
|
46
|
+
() => isTrue(field.questionOptions.isCheckboxSearchable),
|
47
|
+
[field.questionOptions.isCheckboxSearchable],
|
48
|
+
);
|
49
|
+
|
45
50
|
useEffect(() => {
|
46
51
|
if (isFirstRender.current && counter === 1) {
|
47
52
|
setInitiallyCheckedQuestionItems(initiallySelectedQuestionItems.map((item) => item.concept));
|
@@ -89,7 +94,25 @@ const MultiSelect: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
|
|
89
94
|
<>
|
90
95
|
<div className={styles.boldedLabel}>
|
91
96
|
<Layer>
|
92
|
-
{
|
97
|
+
{isSearchable ? (
|
98
|
+
<FilterableMultiSelect
|
99
|
+
placeholder={t('search', 'Search') + '...'}
|
100
|
+
onChange={handleSelectItemsChange}
|
101
|
+
id={t(field.label)}
|
102
|
+
items={selectOptions}
|
103
|
+
initialSelectedItems={initiallySelectedQuestionItems}
|
104
|
+
label={''}
|
105
|
+
titleText={label}
|
106
|
+
key={counter}
|
107
|
+
itemToString={(item) => (item ? item.label : ' ')}
|
108
|
+
disabled={field.isDisabled}
|
109
|
+
invalid={errors.length > 0}
|
110
|
+
invalidText={errors[0]?.message}
|
111
|
+
warn={warnings.length > 0}
|
112
|
+
warnText={warnings[0]?.message}
|
113
|
+
readOnly={field.readonly}
|
114
|
+
/>
|
115
|
+
) : (
|
93
116
|
<CheckboxGroup legendText={label} name={field.id}>
|
94
117
|
{field.questionOptions.answers?.map((value, index) => {
|
95
118
|
return (
|
@@ -105,32 +128,15 @@ const MultiSelect: React.FC<FormFieldInputProps> = ({ field, value, errors, warn
|
|
105
128
|
defaultChecked={initiallyCheckedQuestionItems.some((item) => item === value.concept)}
|
106
129
|
checked={initiallyCheckedQuestionItems.some((item) => item === value.concept)}
|
107
130
|
onBlur={onblur}
|
131
|
+
disabled={value.disable?.isDisabled}
|
108
132
|
/>
|
109
133
|
);
|
110
134
|
})}
|
111
135
|
</CheckboxGroup>
|
112
|
-
) : (
|
113
|
-
<FilterableMultiSelect
|
114
|
-
placeholder={t('search', 'Search') + '...'}
|
115
|
-
onChange={handleSelectItemsChange}
|
116
|
-
id={t(field.label)}
|
117
|
-
items={selectOptions}
|
118
|
-
initialSelectedItems={initiallySelectedQuestionItems}
|
119
|
-
label={''}
|
120
|
-
titleText={<FieldLabel field={field} />}
|
121
|
-
key={counter}
|
122
|
-
itemToString={(item) => (item ? item.label : ' ')}
|
123
|
-
disabled={field.isDisabled}
|
124
|
-
invalid={errors.length > 0}
|
125
|
-
invalidText={errors[0]?.message}
|
126
|
-
warn={warnings.length > 0}
|
127
|
-
warnText={warnings[0]?.message}
|
128
|
-
readOnly={field.readonly}
|
129
|
-
/>
|
130
136
|
)}
|
131
137
|
</Layer>
|
132
138
|
</div>
|
133
|
-
{
|
139
|
+
{isSearchable && (
|
134
140
|
<div className={styles.selectionDisplay}>
|
135
141
|
{value?.length ? (
|
136
142
|
<div className={styles.tagContainer}>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import React from 'react';
|
2
2
|
import { act, render, screen } from '@testing-library/react';
|
3
|
-
import userEvent from '@testing-library/user-event';
|
3
|
+
import userEvent from '@testing-library/user-event';
|
4
4
|
import { type FetchResponse, openmrsFetch, usePatient, useSession } from '@openmrs/esm-framework';
|
5
5
|
import { type FormSchema } from '../../../types';
|
6
6
|
import { mockPatient } from '__mocks__/patient.mock';
|
@@ -66,13 +66,24 @@ describe('MultiSelect Component', () => {
|
|
66
66
|
expect(screen.getByText('Was this visit scheduled?')).toBeInTheDocument();
|
67
67
|
});
|
68
68
|
|
69
|
+
it('should render a checkbox searchable (combobox) for the "multiCheckbox" rendering type', async () => {
|
70
|
+
await renderForm();
|
71
|
+
const user = userEvent.setup();
|
72
|
+
|
73
|
+
const searchableCombobox = screen.getByRole('combobox', { name: /Checkbox searchable/i });
|
74
|
+
expect(searchableCombobox).toBeInTheDocument();
|
75
|
+
await user.click(searchableCombobox);
|
76
|
+
|
77
|
+
expect(screen.getByRole('option', { name: /Option 1/i })).toBeInTheDocument();
|
78
|
+
expect(screen.getByRole('option', { name: /Option 2/i })).toBeInTheDocument();
|
79
|
+
});
|
80
|
+
|
69
81
|
it('should disable checkbox option if the field value depends on evaluates the expression to true', async () => {
|
70
82
|
const user = userEvent.setup();
|
71
83
|
await renderForm();
|
72
84
|
await user.click(screen.getByRole('combobox', { name: /Patient covered by NHIF/i }));
|
73
85
|
await user.click(screen.getByRole('option', { name: /no/i }));
|
74
|
-
|
75
|
-
const unscheduledVisitOption = screen.getByRole('option', { name: /Unscheduled visit early/i });
|
86
|
+
const unscheduledVisitOption = screen.getByRole('checkbox', { name: /Unscheduled visit early/i });
|
76
87
|
expect(unscheduledVisitOption).toHaveAttribute('disabled');
|
77
88
|
});
|
78
89
|
|
@@ -81,8 +92,7 @@ describe('MultiSelect Component', () => {
|
|
81
92
|
await renderForm();
|
82
93
|
await user.click(screen.getByRole('combobox', { name: /patient covered by nhif/i }));
|
83
94
|
await user.click(screen.getByRole('option', { name: /yes/i }));
|
84
|
-
|
85
|
-
const unscheduledVisitOption = screen.getByRole('option', { name: /Unscheduled visit early/i });
|
95
|
+
const unscheduledVisitOption = screen.getByRole('checkbox', { name: /Unscheduled visit early/i });
|
86
96
|
expect(unscheduledVisitOption).not.toBeDisabled();
|
87
97
|
});
|
88
98
|
});
|
package/src/form-engine.test.tsx
CHANGED
@@ -12,7 +12,7 @@ import {
|
|
12
12
|
} from '@openmrs/esm-framework';
|
13
13
|
import { when } from 'jest-when';
|
14
14
|
import * as api from './api';
|
15
|
-
import { assertFormHasAllFields,
|
15
|
+
import { assertFormHasAllFields, findCheckboxGroup, findSelectInput } from './utils/test-utils';
|
16
16
|
import { evaluatePostSubmissionExpression } from './utils/post-submission-action-helper';
|
17
17
|
import { mockPatient } from '__mocks__/patient.mock';
|
18
18
|
import { mockSessionDataResponse } from '__mocks__/session.mock';
|
@@ -155,18 +155,18 @@ describe('Form engine component', () => {
|
|
155
155
|
await assertFormHasAllFields(screen, [
|
156
156
|
{ fieldName: 'When was the HIV test conducted? *', fieldType: 'date' },
|
157
157
|
{ fieldName: 'Community service delivery point', fieldType: 'select' },
|
158
|
-
{ fieldName: 'TB screening', fieldType: '
|
158
|
+
{ fieldName: 'TB screening', fieldType: 'checkbox' },
|
159
159
|
]);
|
160
160
|
});
|
161
161
|
|
162
|
-
it('should demonstrate
|
162
|
+
it('should demonstrate behavior driven by form intents', async () => {
|
163
163
|
await act(async () => {
|
164
164
|
renderForm('955ab92f-f93e-4dc0-9c68-b7b2346def55', null, 'HTS_INTENT_A');
|
165
165
|
});
|
166
166
|
|
167
167
|
await assertFormHasAllFields(screen, [
|
168
168
|
{ fieldName: 'When was the HIV test conducted? *', fieldType: 'date' },
|
169
|
-
{ fieldName: 'TB screening', fieldType: '
|
169
|
+
{ fieldName: 'TB screening', fieldType: 'checkbox' },
|
170
170
|
]);
|
171
171
|
|
172
172
|
try {
|
@@ -188,14 +188,14 @@ describe('Form engine component', () => {
|
|
188
188
|
|
189
189
|
await assertFormHasAllFields(screen, [
|
190
190
|
{ fieldName: 'When was the HIV test conducted? *', fieldType: 'date' },
|
191
|
-
{ fieldName: 'Community service delivery point', fieldType: '
|
191
|
+
{ fieldName: 'Community service delivery point *', fieldType: 'select' },
|
192
192
|
]);
|
193
193
|
|
194
194
|
try {
|
195
|
-
await
|
195
|
+
await findCheckboxGroup(screen, 'TB screening');
|
196
196
|
fail("Field with title 'TB screening' should not be found");
|
197
197
|
} catch (err) {
|
198
|
-
expect(err.message.includes('Unable to find role="
|
198
|
+
expect(err.message.includes('Unable to find role="group" and name `/TB screening/i`')).toBeTruthy();
|
199
199
|
}
|
200
200
|
});
|
201
201
|
|
@@ -262,19 +262,18 @@ describe('Form engine component', () => {
|
|
262
262
|
|
263
263
|
describe('historical expressions', () => {
|
264
264
|
it('should ascertain getPreviousEncounter() returns an encounter and the historical expression displays on the UI', async () => {
|
265
|
-
const user = userEvent.setup();
|
266
|
-
|
267
265
|
renderForm(null, historicalExpressionsForm, 'COVID Assessment');
|
268
266
|
|
269
267
|
//ascertain form has rendered
|
270
|
-
await screen
|
268
|
+
const checkboxGroup = await findCheckboxGroup(screen, 'Reasons for assessment');
|
269
|
+
expect(checkboxGroup).toBeInTheDocument();
|
271
270
|
|
272
271
|
//ascertain function fetching the encounter has been called
|
273
272
|
expect(api.getPreviousEncounter).toHaveBeenCalled();
|
274
273
|
expect(api.getPreviousEncounter).toHaveReturnedWith(Promise.resolve(mockHxpEncounter));
|
275
274
|
|
276
275
|
expect(screen.getByRole('button', { name: /reuse value/i })).toBeInTheDocument;
|
277
|
-
expect(screen.getByText(/Entry into a country/i));
|
276
|
+
expect(screen.getByText(/Entry into a country/i, { selector: 'div.value' }));
|
278
277
|
});
|
279
278
|
});
|
280
279
|
|
@@ -328,14 +327,14 @@ describe('Form engine component', () => {
|
|
328
327
|
await user.click(screen.getByRole('button', { name: /save/i }));
|
329
328
|
|
330
329
|
await assertFormHasAllFields(screen, [
|
331
|
-
{ fieldName: 'Was this visit scheduled?', fieldType: '
|
330
|
+
{ fieldName: 'Was this visit scheduled?', fieldType: 'select' },
|
332
331
|
{ fieldName: 'If Unscheduled, actual text scheduled date *', fieldType: 'text' },
|
333
332
|
{ fieldName: 'If Unscheduled, actual scheduled date *', fieldType: 'date' },
|
334
333
|
{ fieldName: 'If Unscheduled, actual number scheduled date *', fieldType: 'number' },
|
335
334
|
{ fieldName: 'If Unscheduled, actual text area scheduled date *', fieldType: 'textarea' },
|
336
335
|
{ fieldName: 'Not required actual text area scheduled date', fieldType: 'textarea' },
|
337
336
|
{ fieldName: 'If Unscheduled, actual scheduled reason select *', fieldType: 'select' },
|
338
|
-
{ fieldName: 'If Unscheduled, actual scheduled reason multi-select *', fieldType: '
|
337
|
+
{ fieldName: 'If Unscheduled, actual scheduled reason multi-select *', fieldType: 'checkbox-searchable' },
|
339
338
|
{ fieldName: 'If Unscheduled, actual scheduled reason radio *', fieldType: 'radio' },
|
340
339
|
]);
|
341
340
|
|
@@ -544,7 +543,7 @@ describe('Form engine component', () => {
|
|
544
543
|
await act(async () => {
|
545
544
|
renderForm(null, conditionalRequiredTestForm);
|
546
545
|
});
|
547
|
-
await assertFormHasAllFields(screen, [{ fieldName: 'Was this visit scheduled?', fieldType: '
|
546
|
+
await assertFormHasAllFields(screen, [{ fieldName: 'Was this visit scheduled?', fieldType: 'select' }]);
|
548
547
|
await user.click(screen.getByRole('button', { name: /save/i }));
|
549
548
|
expect(saveEncounterMock).toHaveBeenCalled();
|
550
549
|
expect(saveEncounterMock).toHaveBeenCalledWith(expect.any(AbortController), expect.any(Object), undefined);
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { type FormSchema } from '../types';
|
1
2
|
import { DefaultFormSchemaTransformer } from './default-schema-transformer';
|
2
3
|
import testForm from '__mocks__/forms/afe-forms/test-schema-transformer-form.json';
|
3
4
|
|
@@ -20,6 +21,7 @@ const expectedTransformedSchema = {
|
|
20
21
|
id: 'dem_multi_checkbox',
|
21
22
|
questionOptions: {
|
22
23
|
rendering: 'checkbox',
|
24
|
+
isCheckboxSearchable: true,
|
23
25
|
},
|
24
26
|
validators: [
|
25
27
|
{
|
@@ -152,4 +154,58 @@ describe('Default form schema transformer', () => {
|
|
152
154
|
it('should transform AFE schema to be compatible with RFE', () => {
|
153
155
|
expect(DefaultFormSchemaTransformer.transform(testForm as any)).toEqual(expectedTransformedSchema);
|
154
156
|
});
|
157
|
+
|
158
|
+
it('should handle checkbox-searchable rendering', () => {
|
159
|
+
// setup
|
160
|
+
const form = {
|
161
|
+
pages: [
|
162
|
+
{
|
163
|
+
sections: [
|
164
|
+
{
|
165
|
+
questions: [
|
166
|
+
{
|
167
|
+
label: 'Searchable Checkbox',
|
168
|
+
type: 'obs',
|
169
|
+
questionOptions: {
|
170
|
+
rendering: 'checkbox-searchable',
|
171
|
+
},
|
172
|
+
id: 'searchableCheckbox',
|
173
|
+
},
|
174
|
+
],
|
175
|
+
},
|
176
|
+
],
|
177
|
+
},
|
178
|
+
],
|
179
|
+
};
|
180
|
+
// exercise
|
181
|
+
const transformedForm = DefaultFormSchemaTransformer.transform(form as FormSchema);
|
182
|
+
const transformedQuestion = transformedForm.pages[0].sections[0].questions[0];
|
183
|
+
// verify
|
184
|
+
expect(transformedQuestion.questionOptions.rendering).toEqual('checkbox');
|
185
|
+
expect(transformedQuestion.questionOptions.isCheckboxSearchable).toEqual(true);
|
186
|
+
});
|
187
|
+
|
188
|
+
it('should handle multiCheckbox rendering', () => {
|
189
|
+
// setup
|
190
|
+
const form = {
|
191
|
+
pages: [
|
192
|
+
{
|
193
|
+
sections: [
|
194
|
+
{
|
195
|
+
questions: [
|
196
|
+
{
|
197
|
+
label: 'Multi Checkbox',
|
198
|
+
type: 'obs',
|
199
|
+
questionOptions: {
|
200
|
+
rendering: 'multiCheckbox',
|
201
|
+
},
|
202
|
+
id: 'multiCheckboxField',
|
203
|
+
},
|
204
|
+
],
|
205
|
+
},
|
206
|
+
],
|
207
|
+
},
|
208
|
+
],
|
209
|
+
};
|
210
|
+
});
|
155
211
|
});
|
@@ -1,7 +1,9 @@
|
|
1
|
-
import { type FormField, type FormSchemaTransformer, type FormSchema } from '../types';
|
1
|
+
import { type FormField, type FormSchemaTransformer, type FormSchema, type RenderType } from '../types';
|
2
2
|
import { isTrue } from '../utils/boolean-utils';
|
3
3
|
import { hasRendering } from '../utils/common-utils';
|
4
4
|
|
5
|
+
export type RenderTypeExtended = 'multiCheckbox' | 'numeric' | RenderType;
|
6
|
+
|
5
7
|
export const DefaultFormSchemaTransformer: FormSchemaTransformer = {
|
6
8
|
transform: (form: FormSchema) => {
|
7
9
|
parseBooleanTokenIfPresent(form, 'readonly');
|
@@ -135,9 +137,10 @@ function transformByType(question: FormField) {
|
|
135
137
|
}
|
136
138
|
|
137
139
|
function transformByRendering(question: FormField) {
|
138
|
-
switch (question.questionOptions.rendering as
|
140
|
+
switch (question.questionOptions.rendering as RenderTypeExtended) {
|
141
|
+
case 'checkbox-searchable':
|
139
142
|
case 'multiCheckbox':
|
140
|
-
question
|
143
|
+
handleCheckbox(question);
|
141
144
|
break;
|
142
145
|
case 'numeric':
|
143
146
|
question.questionOptions.rendering = 'number';
|
@@ -165,6 +168,20 @@ function transformByRendering(question: FormField) {
|
|
165
168
|
return question;
|
166
169
|
}
|
167
170
|
|
171
|
+
function handleCheckbox(question: FormField) {
|
172
|
+
if ((question.questionOptions.rendering as RenderTypeExtended) === 'multiCheckbox') {
|
173
|
+
question.questionOptions.rendering = 'checkbox-searchable';
|
174
|
+
if (isTrue(question.inlineMultiCheckbox)) {
|
175
|
+
question.questionOptions.rendering = 'checkbox';
|
176
|
+
}
|
177
|
+
}
|
178
|
+
|
179
|
+
if (hasRendering(question, 'checkbox-searchable')) {
|
180
|
+
question.questionOptions.rendering = 'checkbox';
|
181
|
+
question.questionOptions.isCheckboxSearchable = true;
|
182
|
+
}
|
183
|
+
}
|
184
|
+
|
168
185
|
function handleLabOrders(question: FormField) {
|
169
186
|
if (hasRendering(question, 'group') && question.questions?.length) {
|
170
187
|
question.questions.forEach(handleLabOrders);
|
package/src/types/schema.ts
CHANGED
@@ -83,6 +83,7 @@ export interface FormField {
|
|
83
83
|
questionInfo?: string;
|
84
84
|
historicalExpression?: string;
|
85
85
|
constrainMaxWidth?: boolean;
|
86
|
+
/** @deprecated */
|
86
87
|
inlineMultiCheckbox?: boolean;
|
87
88
|
meta?: QuestionMetaProps;
|
88
89
|
}
|
@@ -161,7 +162,14 @@ export interface FormQuestionOptions {
|
|
161
162
|
allowedFileTypes?: Array<string>;
|
162
163
|
allowMultiple?: boolean;
|
163
164
|
datasource?: { name: string; config?: Record<string, any> };
|
165
|
+
/**
|
166
|
+
* Determines if the ui-select-extended rendering is searchable
|
167
|
+
*/
|
164
168
|
isSearchable?: boolean;
|
169
|
+
/**
|
170
|
+
* Determines if the checkbox rendering is searchable
|
171
|
+
*/
|
172
|
+
isCheckboxSearchable?: boolean;
|
165
173
|
workspaceName?: string;
|
166
174
|
buttonLabel?: string;
|
167
175
|
identifierType?: string;
|
@@ -183,8 +191,10 @@ export interface QuestionAnswerOption {
|
|
183
191
|
concept?: string;
|
184
192
|
[key: string]: any;
|
185
193
|
}
|
194
|
+
|
186
195
|
export type RenderType =
|
187
196
|
| 'checkbox'
|
197
|
+
| 'checkbox-searchable'
|
188
198
|
| 'content-switcher'
|
189
199
|
| 'date'
|
190
200
|
| 'datetime'
|
package/src/utils/test-utils.ts
CHANGED
@@ -14,7 +14,11 @@ export async function findRadioGroupMember(screen, name: string): Promise<HTMLIn
|
|
14
14
|
return await screen.findByRole('radio', { name });
|
15
15
|
}
|
16
16
|
|
17
|
-
export async function
|
17
|
+
export async function findCheckboxGroup(screen, name: string): Promise<HTMLInputElement> {
|
18
|
+
return await screen.findByRole('group', { name: new RegExp(name, 'i') });
|
19
|
+
}
|
20
|
+
|
21
|
+
export async function findCheckboxSearchable(screen, nameSubstring: string): Promise<HTMLInputElement> {
|
18
22
|
return await screen.findByRole('combobox', { name: new RegExp(nameSubstring, 'i') });
|
19
23
|
}
|
20
24
|
|
@@ -42,7 +46,8 @@ const fieldTypeToGetterMap = {
|
|
42
46
|
'radio-group': findRadioGroupInput,
|
43
47
|
'radio-item': findRadioGroupMember,
|
44
48
|
textarea: findTextOrDateInput,
|
45
|
-
|
49
|
+
checkbox: findCheckboxGroup,
|
50
|
+
'checkbox-searchable': findCheckboxSearchable,
|
46
51
|
select: findSelectInput,
|
47
52
|
};
|
48
53
|
|