@openmrs/esm-form-engine-lib 2.1.0-pre.1401 → 2.1.0-pre.1404

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-form-engine-lib",
3
- "version": "2.1.0-pre.1401",
3
+ "version": "2.1.0-pre.1404",
4
4
  "description": "React Form Engine for O3",
5
5
  "browser": "dist/openmrs-esm-form-engine-lib.js",
6
6
  "main": "src/index.ts",
@@ -6,29 +6,43 @@ import { isTrue } from '../../../utils/boolean-utils';
6
6
  import { type FormFieldInputProps } from '../../../types';
7
7
  import FieldValueView from '../../value/view/field-value-view.component';
8
8
  import FieldLabel from '../../field-label/field-label.component';
9
-
10
- import styles from './dropdown.scss';
11
9
  import { useFormProviderContext } from '../../../provider/form-provider';
10
+ import { NullSelectOption } from '../../../constants';
11
+ import { isEmpty } from '../../../validators/form-validator';
12
+ import styles from './dropdown.scss';
12
13
 
13
14
  const Dropdown: React.FC<FormFieldInputProps> = ({ field, value, errors, warnings, setFieldValue }) => {
14
15
  const { t } = useTranslation();
15
16
  const { layoutType, sessionMode, workspaceLayout, formFieldAdapters } = useFormProviderContext();
16
17
 
17
18
  const handleChange = useCallback(
18
- (value) => {
19
- setFieldValue(value);
19
+ ({ selectedItem }) => {
20
+ setFieldValue(selectedItem === NullSelectOption ? null : selectedItem);
20
21
  },
21
22
  [setFieldValue],
22
23
  );
23
24
 
24
25
  const itemToString = useCallback(
25
26
  (item) => {
26
- const answer = field.questionOptions.answers.find((opt) => (opt.value ? opt.value == item : opt.concept == item));
27
+ let answer = field.questionOptions.answers.find((opt) => {
28
+ return opt.value ? opt.value == item : opt.concept == item;
29
+ });
27
30
  return answer?.label;
28
31
  },
29
32
  [field.questionOptions.answers],
30
33
  );
31
34
 
35
+ const items = useMemo(() => {
36
+ const options = field.questionOptions.answers;
37
+ if (!options.some((option) => option.value === NullSelectOption)) {
38
+ options.unshift({
39
+ value: NullSelectOption,
40
+ label: t('chooseAnOption', 'Choose an option'),
41
+ });
42
+ }
43
+ return options.filter((option) => !option.isHidden).map((item) => item.value || item.concept);
44
+ }, [field.questionOptions.answers]);
45
+
32
46
  const isInline = useMemo(() => {
33
47
  if (['view', 'embedded-view'].includes(sessionMode) || isTrue(field.readonly)) {
34
48
  return shouldUseInlineLayout(field.inlineRendering, layoutType, workspaceLayout, sessionMode);
@@ -50,13 +64,10 @@ const Dropdown: React.FC<FormFieldInputProps> = ({ field, value, errors, warning
50
64
  <DropdownInput
51
65
  id={field.id}
52
66
  titleText={<FieldLabel field={field} />}
53
- label={t('chooseAnOption', 'Choose an option')}
54
- items={field.questionOptions.answers
55
- .filter((answer) => !answer.isHidden)
56
- .map((item) => item.value || item.concept)}
67
+ items={items}
57
68
  itemToString={itemToString}
58
- selectedItem={value}
59
- onChange={({ selectedItem }) => handleChange(selectedItem)}
69
+ selectedItem={isEmpty(value) ? NullSelectOption : value}
70
+ onChange={handleChange}
60
71
  disabled={field.isDisabled}
61
72
  readOnly={field.readonly}
62
73
  invalid={errors.length > 0}
@@ -117,4 +117,41 @@ describe.skip('dropdown input field', () => {
117
117
  });
118
118
  });
119
119
  });
120
- });
120
+
121
+ it('should clear selection when empty option is selected', async () => {
122
+ // setup
123
+ question.meta.previousValue = {
124
+ uuid: '305ed1fc-c1fd-11eb-8529-0242ac130003',
125
+ person: '833db896-c1f0-11eb-8529-0242ac130003',
126
+ obsDatetime: encounterContext.encounterDate,
127
+ concept: '1c43b05b-b6d8-4eb5-8f37-0b14f5347568',
128
+ location: { uuid: '41e6e516-c1f0-11eb-8529-0242ac130003' },
129
+ order: null,
130
+ groupMembers: [],
131
+ voided: false,
132
+ value: '6ddd933a-e65c-4f35-8884-c555b50c55e1',
133
+ };
134
+ await renderForm({ 'patient-past-program': question.meta.previousValue.value });
135
+ const dropdownWidget = screen.getByRole('combobox', { name: /Patient past program./ });
136
+
137
+ // select an option first
138
+ fireEvent.click(dropdownWidget);
139
+ const oncologyScreeningOption = screen.getByText('Oncology Screening and Diagnosis Program');
140
+ fireEvent.click(oncologyScreeningOption);
141
+
142
+ // clear the selection
143
+ fireEvent.click(dropdownWidget);
144
+ const clearOption = screen.getByText('Select an option');
145
+ fireEvent.click(clearOption);
146
+
147
+ // verify
148
+ await act(async () => {
149
+ expect(question.meta.submission.newValue).toEqual({
150
+ uuid: '305ed1fc-c1fd-11eb-8529-0242ac130003',
151
+ value: null,
152
+ formFieldNamespace: 'rfe-forms',
153
+ formFieldPath: 'rfe-forms-patient-past-program',
154
+ });
155
+ });
156
+ });
157
+ });
package/src/constants.ts CHANGED
@@ -10,3 +10,4 @@ export const encounterRepresentation =
10
10
  export const FormsStore = 'forms-engine-store';
11
11
  export const PatientChartWorkspaceHeaderSlot = 'patient-chart-workspace-header-slot';
12
12
  export const codedTypes = ['radio', 'checkbox', 'select', 'content-switcher'];
13
+ export const NullSelectOption = 'OPTION_NULL';
@@ -0,0 +1,76 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
+
3
+ import useInitialValues from './useInitialValues';
4
+ import { type FormField } from '../types';
5
+
6
+ export const mockFormFields: FormField[] = [
7
+ {
8
+ label: 'First Name',
9
+ type: 'text',
10
+ id: 'firstName',
11
+ questionOptions: {
12
+ rendering: 'text',
13
+ },
14
+ value: '',
15
+ isRequired: true,
16
+ validators: [{ required: true }],
17
+ },
18
+ {
19
+ label: 'Date of Birth',
20
+ type: 'date',
21
+ questionOptions: {
22
+ rendering: 'date',
23
+ },
24
+ datePickerFormat: 'both',
25
+ id: 'dob',
26
+ value: null,
27
+ isRequired: true,
28
+ validators: [{ required: true }],
29
+ },
30
+ ];
31
+
32
+ describe('useInitialValues', () => {
33
+ let formProcessor;
34
+ let context;
35
+
36
+ beforeEach(() => {
37
+ formProcessor = {
38
+ getInitialValues: jest.fn(),
39
+ };
40
+ context = {
41
+ formFields: mockFormFields,
42
+ formFieldAdapters: { adapter1: {}, adapter2: {} },
43
+ };
44
+ });
45
+
46
+ it('should not call getInitialValues if context dependencies are not loaded', () => {
47
+ const { result } = renderHook(() => useInitialValues(formProcessor, true, context));
48
+
49
+ expect(result.current.isLoadingInitialValues).toBe(true);
50
+ expect(formProcessor.getInitialValues).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('should set error if getInitialValues rejects', async () => {
54
+ const mockError = new Error('Failed to fetch initial values');
55
+ formProcessor.getInitialValues.mockRejectedValue(mockError);
56
+
57
+ const { result } = await act(() => renderHook(() => useInitialValues(formProcessor, false, context)));
58
+
59
+ expect(result.current.error).toEqual(mockError);
60
+ expect(result.current.isLoadingInitialValues).toBe(false);
61
+ });
62
+
63
+ it('should set initial values when dependencies are loaded', async () => {
64
+ const mockInitialValues = { firstName: 'John Doe', dob: '1992-10-10' };
65
+ formProcessor.getInitialValues.mockResolvedValue(mockInitialValues);
66
+
67
+ const { result } = renderHook(() => useInitialValues(formProcessor, false, context));
68
+
69
+ await waitFor(() => expect(result.current.isLoadingInitialValues).toBe(true));
70
+
71
+ expect(formProcessor.getInitialValues).toHaveBeenCalledWith(context);
72
+ expect(result.current.initialValues).toEqual(mockInitialValues);
73
+ expect(result.current.isLoadingInitialValues).toBe(false);
74
+ expect(result.current.error).toBe(null);
75
+ });
76
+ });
@@ -28,6 +28,7 @@ const useInitialValues = (
28
28
  .catch((error) => {
29
29
  console.error(error);
30
30
  setError(error);
31
+ setIsLoadingInitialValues(false);
31
32
  });
32
33
  }
33
34
  }, [formProcessor, isLoadingContextDependencies, context]);