@openmrs/esm-form-engine-lib 2.1.0-pre.1475 → 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.
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-form-engine-lib",
3
- "version": "2.1.0-pre.1475",
3
+ "version": "2.1.0-pre.1476",
4
4
  "description": "React Form Engine for O3",
5
5
  "browser": "dist/openmrs-esm-form-engine-lib.js",
6
6
  "main": "src/index.ts",
@@ -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
- {field.inlineMultiCheckbox ? (
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
- {!field.inlineMultiCheckbox && (
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'; // Correct import for userEvent
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
- await user.click(screen.getByText('Was this visit scheduled?'));
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
- await user.click(screen.getByText('Was this visit scheduled?'));
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
  });
@@ -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, findMultiSelectInput, findSelectInput } from './utils/test-utils';
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: 'combobox' },
158
+ { fieldName: 'TB screening', fieldType: 'checkbox' },
159
159
  ]);
160
160
  });
161
161
 
162
- it('should demonstrate behaviour driven by form intents', async () => {
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: 'combobox' },
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: 'combobox' },
191
+ { fieldName: 'Community service delivery point *', fieldType: 'select' },
192
192
  ]);
193
193
 
194
194
  try {
195
- await findMultiSelectInput(screen, 'TB screening');
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="combobox" and name `/TB screening/i`')).toBeTruthy();
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.findByRole('combobox', { name: /Reasons for assessment/i });
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: 'combobox' },
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: 'combobox' },
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: 'combobox' }]);
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 any) {
140
+ switch (question.questionOptions.rendering as RenderTypeExtended) {
141
+ case 'checkbox-searchable':
139
142
  case 'multiCheckbox':
140
- question.questionOptions.rendering = 'checkbox';
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);
@@ -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'
@@ -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 findMultiSelectInput(screen, nameSubstring: string): Promise<HTMLInputElement> {
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
- combobox: findMultiSelectInput,
49
+ checkbox: findCheckboxGroup,
50
+ 'checkbox-searchable': findCheckboxSearchable,
46
51
  select: findSelectInput,
47
52
  };
48
53