@kenyaemr/esm-patient-registration-app 4.3.0
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/README.md +7 -0
- package/__mocks__/autogenerationoptions.mock.ts +34 -0
- package/__mocks__/react-i18next.js +49 -0
- package/dist/144.js +2 -0
- package/dist/144.js.LICENSE.txt +27 -0
- package/dist/144.js.map +1 -0
- package/dist/207.js +1 -0
- package/dist/207.js.map +1 -0
- package/dist/317.js +2 -0
- package/dist/317.js.LICENSE.txt +6 -0
- package/dist/317.js.map +1 -0
- package/dist/330.js +1 -0
- package/dist/330.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/59.js +1 -0
- package/dist/59.js.map +1 -0
- package/dist/591.js +2 -0
- package/dist/591.js.LICENSE.txt +32 -0
- package/dist/591.js.map +1 -0
- package/dist/62.js +1 -0
- package/dist/62.js.map +1 -0
- package/dist/635.js +1 -0
- package/dist/635.js.map +1 -0
- package/dist/68.js +1 -0
- package/dist/68.js.map +1 -0
- package/dist/735.js +1 -0
- package/dist/735.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/784.js +2 -0
- package/dist/784.js.LICENSE.txt +9 -0
- package/dist/784.js.map +1 -0
- package/dist/805.js +1 -0
- package/dist/805.js.map +1 -0
- package/dist/807.js +1 -0
- package/dist/821.js +1 -0
- package/dist/821.js.map +1 -0
- package/dist/822.js +1 -0
- package/dist/822.js.map +1 -0
- package/dist/858.js +2 -0
- package/dist/858.js.LICENSE.txt +3 -0
- package/dist/858.js.map +1 -0
- package/dist/887.js +1 -0
- package/dist/887.js.map +1 -0
- package/dist/9.js +2 -0
- package/dist/9.js.LICENSE.txt +9 -0
- package/dist/9.js.map +1 -0
- package/dist/975.js +1 -0
- package/dist/975.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +9 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-patient-registration-app.js +1 -0
- package/dist/openmrs-esm-patient-registration-app.js.buildmanifest.json +623 -0
- package/dist/openmrs-esm-patient-registration-app.js.map +1 -0
- package/dist/openmrs-esm-patient-registration-app.old +1 -0
- package/docs/images/patient-registration-hierarchy.png +0 -0
- package/package.json +55 -0
- package/src/add-patient-link.scss +3 -0
- package/src/add-patient-link.tsx +21 -0
- package/src/config-schema.ts +405 -0
- package/src/constants.ts +14 -0
- package/src/declarations.d.tsx +4 -0
- package/src/index.ts +131 -0
- package/src/nav-link.tsx +10 -0
- package/src/offline.resources.ts +109 -0
- package/src/offline.ts +90 -0
- package/src/patient-registration/before-save-prompt.tsx +72 -0
- package/src/patient-registration/date-util.ts +52 -0
- package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
- package/src/patient-registration/field/address/address-field.component.tsx +31 -0
- package/src/patient-registration/field/address/address-hierarchy.component.tsx +143 -0
- package/src/patient-registration/field/address/address-hierarchy.test.tsx +181 -0
- package/src/patient-registration/field/address/address-search.component.tsx +98 -0
- package/src/patient-registration/field/address/address-search.scss +53 -0
- package/src/patient-registration/field/custom-field.component.tsx +25 -0
- package/src/patient-registration/field/dob/dob.component.tsx +143 -0
- package/src/patient-registration/field/dob/dob.test.tsx +73 -0
- package/src/patient-registration/field/field.component.tsx +44 -0
- package/src/patient-registration/field/field.resource.ts +35 -0
- package/src/patient-registration/field/field.scss +127 -0
- package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
- package/src/patient-registration/field/gender/gender-field.test.tsx +66 -0
- package/src/patient-registration/field/id/id-field.component.tsx +142 -0
- package/src/patient-registration/field/id/identifier-selection-overlay.tsx +194 -0
- package/src/patient-registration/field/id/identifier-selection.scss +37 -0
- package/src/patient-registration/field/name/name-field.component.tsx +109 -0
- package/src/patient-registration/field/obs/obs-field.component.tsx +185 -0
- package/src/patient-registration/field/obs/obs-field.test.tsx +127 -0
- package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +59 -0
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +68 -0
- package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +81 -0
- package/src/patient-registration/field/person-attributes/person-attributes.resource.tsx +20 -0
- package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +57 -0
- package/src/patient-registration/form-manager.test.ts +68 -0
- package/src/patient-registration/form-manager.ts +413 -0
- package/src/patient-registration/input/basic-input/input/input.component.tsx +59 -0
- package/src/patient-registration/input/basic-input/input/input.test.tsx +170 -0
- package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
- package/src/patient-registration/input/basic-input/select/select-input.test.tsx +32 -0
- package/src/patient-registration/input/combo-input/combo-input.component.tsx +76 -0
- package/src/patient-registration/input/combo-input/combo-input.test.tsx +43 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +84 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +53 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +109 -0
- package/src/patient-registration/input/custom-input/estimated-age/estimated-age-input.component.tsx +32 -0
- package/src/patient-registration/input/custom-input/estimated-age/estimated-age-input.test.tsx +36 -0
- package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
- package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +110 -0
- package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
- package/src/patient-registration/input/custom-input/unidentified-patient/unidentified-patient-input.component.tsx +24 -0
- package/src/patient-registration/input/custom-input/unidentified-patient/unidentified-patient-input.test.tsx +39 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
- package/src/patient-registration/input/input.scss +108 -0
- package/src/patient-registration/patient-registration-context.ts +24 -0
- package/src/patient-registration/patient-registration-hooks.ts +320 -0
- package/src/patient-registration/patient-registration-types.tsx +271 -0
- package/src/patient-registration/patient-registration-utils.ts +219 -0
- package/src/patient-registration/patient-registration.component.tsx +250 -0
- package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
- package/src/patient-registration/patient-registration.resource.tsx +296 -0
- package/src/patient-registration/patient-registration.scss +94 -0
- package/src/patient-registration/patient-registration.test.tsx +436 -0
- package/src/patient-registration/section/death-info/death-info-section.component.tsx +30 -0
- package/src/patient-registration/section/death-info/death-info-section.test.tsx +73 -0
- package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
- package/src/patient-registration/section/demographics/demographics-section.test.tsx +84 -0
- package/src/patient-registration/section/generic-section.component.tsx +17 -0
- package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +226 -0
- package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
- package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
- package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
- package/src/patient-registration/section/section.component.tsx +23 -0
- package/src/patient-registration/section/section.scss +1 -0
- package/src/patient-registration/ui-components/overlay/index.tsx +51 -0
- package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
- package/src/patient-registration/validation/patient-registration-validation.test.tsx +129 -0
- package/src/patient-registration/validation/patient-registration-validation.tsx +46 -0
- package/src/patient-verification/assets/counties.json +236 -0
- package/src/patient-verification/assets/verification-assets.ts +11 -0
- package/src/patient-verification/patient-verification-hook.tsx +156 -0
- package/src/patient-verification/patient-verification-utils.ts +173 -0
- package/src/patient-verification/patient-verification.component.tsx +118 -0
- package/src/patient-verification/patient-verification.scss +30 -0
- package/src/patient-verification/verification-modal/confirm-prompt.component.tsx +69 -0
- package/src/patient-verification/verification-modal/empty-prompt.component.tsx +35 -0
- package/src/patient-verification/verification-types.ts +50 -0
- package/src/resource.ts +12 -0
- package/src/root.component.tsx +66 -0
- package/src/root.scss +7 -0
- package/src/root.test.tsx +32 -0
- package/src/widgets/cancel-patient-edit.component.tsx +37 -0
- package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
- package/src/widgets/delete-identifier-modal.scss +34 -0
- package/src/widgets/display-photo.component.tsx +30 -0
- package/src/widgets/edit-patient-details-button.component.tsx +34 -0
- package/src/widgets/edit-patient-details-button.scss +3 -0
- package/translations/en.json +108 -0
- package/translations/fr.json +89 -0
- package/translations/km.json +89 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { Formik, Form } from 'formik';
|
|
5
|
+
import { Input } from './input.component';
|
|
6
|
+
|
|
7
|
+
describe.skip('number input', () => {
|
|
8
|
+
const setupInput = async () => {
|
|
9
|
+
render(
|
|
10
|
+
<Formik initialValues={{ number: 0 }} onSubmit={null}>
|
|
11
|
+
<Form>
|
|
12
|
+
<Input id="number" labelText="Number" name="number" />
|
|
13
|
+
</Form>
|
|
14
|
+
</Formik>,
|
|
15
|
+
);
|
|
16
|
+
return screen.getByLabelText('number') as HTMLInputElement;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
it('exists', async () => {
|
|
20
|
+
const input = await setupInput();
|
|
21
|
+
expect(input.type).toEqual('number');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('can input data', async () => {
|
|
25
|
+
const user = userEvent.setup();
|
|
26
|
+
|
|
27
|
+
const input = await setupInput();
|
|
28
|
+
const expected = 1;
|
|
29
|
+
|
|
30
|
+
await user.type(input, expected.toString());
|
|
31
|
+
await user.tab();
|
|
32
|
+
|
|
33
|
+
expect(input.valueAsNumber).toEqual(expected);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe.skip('text input', () => {
|
|
38
|
+
const setupInput = async () => {
|
|
39
|
+
render(
|
|
40
|
+
<Formik initialValues={{ text: '' }} onSubmit={null}>
|
|
41
|
+
<Form>
|
|
42
|
+
<Input id="text" labelText="Text" name="text" placeholder="Enter text" />
|
|
43
|
+
</Form>
|
|
44
|
+
</Formik>,
|
|
45
|
+
);
|
|
46
|
+
return screen.getByLabelText('text') as HTMLInputElement;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
it('exists', async () => {
|
|
50
|
+
const input = await setupInput();
|
|
51
|
+
expect(input.type).toEqual('text');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('can input data', async () => {
|
|
55
|
+
const user = userEvent.setup();
|
|
56
|
+
|
|
57
|
+
const input = await setupInput();
|
|
58
|
+
const expected = 'Some text';
|
|
59
|
+
|
|
60
|
+
await user.type(input, expected);
|
|
61
|
+
await user.tab();
|
|
62
|
+
|
|
63
|
+
expect(input.value).toEqual(expected);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe.skip('telephone number input', () => {
|
|
68
|
+
const setupInput = async () => {
|
|
69
|
+
render(
|
|
70
|
+
<Formik initialValues={{ telephoneNumber: '' }} onSubmit={null}>
|
|
71
|
+
<Form>
|
|
72
|
+
<Input id="tel" labelText="Telephone Number" name="telephoneNumber" placeholder="Enter telephone number" />
|
|
73
|
+
</Form>
|
|
74
|
+
</Formik>,
|
|
75
|
+
);
|
|
76
|
+
return screen.getByLabelText('telephoneNumber') as HTMLInputElement;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
it('exists', async () => {
|
|
80
|
+
const input = await setupInput();
|
|
81
|
+
expect(input.type).toEqual('tel');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('can input data', async () => {
|
|
85
|
+
const user = userEvent.setup();
|
|
86
|
+
|
|
87
|
+
const input = await setupInput();
|
|
88
|
+
const expected = '0800001066';
|
|
89
|
+
|
|
90
|
+
await user.type(input, expected);
|
|
91
|
+
await user.tab();
|
|
92
|
+
|
|
93
|
+
expect(input.value).toEqual(expected);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe.skip('date input', () => {
|
|
98
|
+
const setupInput = async () => {
|
|
99
|
+
render(
|
|
100
|
+
<Formik initialValues={{ date: '' }} onSubmit={null}>
|
|
101
|
+
<Form>
|
|
102
|
+
<Input id="date" labelText="date" name="date" />
|
|
103
|
+
</Form>
|
|
104
|
+
</Formik>,
|
|
105
|
+
);
|
|
106
|
+
return screen.getByLabelText('date') as HTMLInputElement;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
it('exists', async () => {
|
|
110
|
+
const input = await setupInput();
|
|
111
|
+
expect(input.type).toEqual('date');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('can input data', async () => {
|
|
115
|
+
const user = userEvent.setup();
|
|
116
|
+
|
|
117
|
+
const input = await setupInput();
|
|
118
|
+
const expected = '1990-09-10';
|
|
119
|
+
|
|
120
|
+
await user.type(input, expected);
|
|
121
|
+
await user.tab();
|
|
122
|
+
|
|
123
|
+
expect(input.value).toEqual(expected);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe.skip('checkbox input', () => {
|
|
128
|
+
const setupInput = async () => {
|
|
129
|
+
render(
|
|
130
|
+
<Formik initialValues={{ checkbox: false }} onSubmit={null}>
|
|
131
|
+
<Form>
|
|
132
|
+
<Input id="checkbox" labelText="checkbox" name="checkbox" />
|
|
133
|
+
</Form>
|
|
134
|
+
</Formik>,
|
|
135
|
+
);
|
|
136
|
+
return screen.getByLabelText('checkbox') as HTMLInputElement;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
it('exists', async () => {
|
|
140
|
+
const input = await setupInput();
|
|
141
|
+
expect(input.type).toEqual('checkbox');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('can input data', async () => {
|
|
145
|
+
const user = userEvent.setup();
|
|
146
|
+
const input = await setupInput();
|
|
147
|
+
|
|
148
|
+
const expected = true;
|
|
149
|
+
|
|
150
|
+
await user.click(input);
|
|
151
|
+
await user.tab();
|
|
152
|
+
|
|
153
|
+
expect(input.checked).toEqual(expected);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('can update data', async () => {
|
|
157
|
+
const user = userEvent.setup();
|
|
158
|
+
const input = await setupInput();
|
|
159
|
+
|
|
160
|
+
const expected = false;
|
|
161
|
+
|
|
162
|
+
await user.click(input);
|
|
163
|
+
await user.tab();
|
|
164
|
+
|
|
165
|
+
await user.click(input);
|
|
166
|
+
await user.tab();
|
|
167
|
+
|
|
168
|
+
expect(input.checked).toEqual(expected);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Layer, Select, SelectItem } from '@carbon/react';
|
|
3
|
+
import { useField } from 'formik';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
interface SelectInputProps {
|
|
7
|
+
name: string;
|
|
8
|
+
options: Array<string>;
|
|
9
|
+
label: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SelectInput: React.FC<SelectInputProps> = ({ name, options, label, required }) => {
|
|
14
|
+
const [field] = useField(name);
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const selectOptions = [
|
|
17
|
+
<SelectItem disabled hidden text={`Select ${label}`} key="" value="" />,
|
|
18
|
+
...options.map((currentOption, index) => <SelectItem text={currentOption} value={currentOption} key={index} />),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const labelText = required ? label : `${label} (${t('optional', 'optional')})`;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div style={{ marginBottom: '1rem' }}>
|
|
25
|
+
<Layer>
|
|
26
|
+
<Select id="identifier" {...field} labelText={labelText}>
|
|
27
|
+
{selectOptions}
|
|
28
|
+
</Select>
|
|
29
|
+
</Layer>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
|
|
3
|
+
import { Formik, Form } from 'formik';
|
|
4
|
+
import { SelectInput } from './select-input.component';
|
|
5
|
+
|
|
6
|
+
describe.skip('select input', () => {
|
|
7
|
+
const setupSelect = async () => {
|
|
8
|
+
render(
|
|
9
|
+
<Formik initialValues={{ select: '' }} onSubmit={null}>
|
|
10
|
+
<Form>
|
|
11
|
+
<SelectInput label="Select" name="select" options={['A Option', 'B Option']} />
|
|
12
|
+
</Form>
|
|
13
|
+
</Formik>,
|
|
14
|
+
);
|
|
15
|
+
return screen.getByLabelText('select') as HTMLInputElement;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
it('exists', async () => {
|
|
19
|
+
const input = await setupSelect();
|
|
20
|
+
expect(input.type).toEqual('select-one');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('can input data', async () => {
|
|
24
|
+
const input = await setupSelect();
|
|
25
|
+
const expected = 'A Option';
|
|
26
|
+
|
|
27
|
+
fireEvent.change(input, { target: { value: expected } });
|
|
28
|
+
fireEvent.blur(input);
|
|
29
|
+
|
|
30
|
+
await waitFor(() => expect(input.value).toEqual(expected));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { ComboBox, ComboBoxProps, Layer } from '@carbon/react';
|
|
3
|
+
import { useField } from 'formik';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { performAdressHierarchyWithParentSearch } from '../../../resource';
|
|
6
|
+
|
|
7
|
+
interface ComboInputProps extends Omit<ComboBoxProps, 'items'> {
|
|
8
|
+
name: string;
|
|
9
|
+
labelText: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
setSelectedValue: any;
|
|
12
|
+
selected: string;
|
|
13
|
+
textFieldName: string;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const ComboInput: React.FC<ComboInputProps> = ({
|
|
18
|
+
name,
|
|
19
|
+
labelText,
|
|
20
|
+
placeholder,
|
|
21
|
+
setSelectedValue,
|
|
22
|
+
selected,
|
|
23
|
+
textFieldName,
|
|
24
|
+
required,
|
|
25
|
+
}) => {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const [field, _, helpers] = useField(name);
|
|
28
|
+
const [listItems, setListItems] = useState([]);
|
|
29
|
+
const [error, setError] = useState<Error>();
|
|
30
|
+
const { setValue } = helpers;
|
|
31
|
+
const comboLabelText = !required ? `${labelText} (${t('optional', 'optional')})` : labelText;
|
|
32
|
+
const comboBoxEvent = (text, id) => {
|
|
33
|
+
if (text == '') {
|
|
34
|
+
} else {
|
|
35
|
+
performAdressHierarchyWithParentSearch(textFieldName.replace(' ', ''), selected, text)
|
|
36
|
+
.then((value) => {
|
|
37
|
+
setListItems(value.data.map((parent1) => ({ id: parent1['uuid'], text: parent1['name'] })));
|
|
38
|
+
})
|
|
39
|
+
.catch((err) => {
|
|
40
|
+
setError(err);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (error) {
|
|
46
|
+
return <span>{t(`${error.message}`)}</span>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div style={{ marginBottom: '1rem' }}>
|
|
51
|
+
<Layer>
|
|
52
|
+
<ComboBox
|
|
53
|
+
id={name}
|
|
54
|
+
onInputChange={(event) => {
|
|
55
|
+
comboBoxEvent(event, name);
|
|
56
|
+
setValue(event);
|
|
57
|
+
}}
|
|
58
|
+
items={listItems}
|
|
59
|
+
itemToString={(item) => item?.text ?? ''}
|
|
60
|
+
{...field}
|
|
61
|
+
onChange={(e) => {
|
|
62
|
+
if (Boolean(e.selectedItem)) {
|
|
63
|
+
setSelectedValue(e.selectedItem.id);
|
|
64
|
+
setValue(e.selectedItem.text);
|
|
65
|
+
} else {
|
|
66
|
+
setSelectedValue(undefined);
|
|
67
|
+
setValue(undefined);
|
|
68
|
+
}
|
|
69
|
+
}}
|
|
70
|
+
placeholder={placeholder}
|
|
71
|
+
titleText={comboLabelText}
|
|
72
|
+
/>
|
|
73
|
+
</Layer>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { Form, Formik } from 'formik';
|
|
3
|
+
import { ComboInput } from './combo-input.component';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
describe('Combo box input', () => {
|
|
7
|
+
const setupInput = async () => {
|
|
8
|
+
const selected = null;
|
|
9
|
+
const setSelectedValue = null;
|
|
10
|
+
render(
|
|
11
|
+
<Formik initialValues={{ text: '' }} onSubmit={null}>
|
|
12
|
+
<Form>
|
|
13
|
+
<ComboInput
|
|
14
|
+
selected={''}
|
|
15
|
+
setSelectedValue={setSelectedValue}
|
|
16
|
+
id="text"
|
|
17
|
+
labelText="Text"
|
|
18
|
+
name="text"
|
|
19
|
+
textFieldName="text"
|
|
20
|
+
placeholder="Enter text"
|
|
21
|
+
required
|
|
22
|
+
/>
|
|
23
|
+
</Form>
|
|
24
|
+
</Formik>,
|
|
25
|
+
);
|
|
26
|
+
return screen.getByLabelText('Text') as HTMLInputElement;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it('exists', async () => {
|
|
30
|
+
const input = await setupInput();
|
|
31
|
+
expect(input.type).toEqual('text');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('can input data', async () => {
|
|
35
|
+
const input = await setupInput();
|
|
36
|
+
const expected = 'Some Text';
|
|
37
|
+
|
|
38
|
+
fireEvent.change(input, { target: { value: expected } });
|
|
39
|
+
fireEvent.blur(input);
|
|
40
|
+
|
|
41
|
+
await waitFor(() => expect(input.value).toEqual(expected));
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Layer, Search, SearchProps } from '@carbon/react';
|
|
4
|
+
import styles from './autosuggest.scss';
|
|
5
|
+
|
|
6
|
+
interface AutosuggestProps extends SearchProps {
|
|
7
|
+
getDisplayValue: Function;
|
|
8
|
+
getFieldValue: Function;
|
|
9
|
+
getSearchResults: (query: string) => Promise<any>;
|
|
10
|
+
onSuggestionSelected: (field: string, value: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Autosuggest: React.FC<any> = ({
|
|
14
|
+
getDisplayValue,
|
|
15
|
+
getFieldValue,
|
|
16
|
+
getSearchResults,
|
|
17
|
+
onSuggestionSelected,
|
|
18
|
+
...searchProps
|
|
19
|
+
}) => {
|
|
20
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
21
|
+
const searchBox = useRef(null);
|
|
22
|
+
const wrapper = useRef(null);
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const { name, labelText } = searchProps;
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
document.addEventListener('mousedown', handleClickOutsideComponent);
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
document.removeEventListener('mousedown', handleClickOutsideComponent);
|
|
31
|
+
};
|
|
32
|
+
}, [wrapper]);
|
|
33
|
+
|
|
34
|
+
const handleClickOutsideComponent = (e) => {
|
|
35
|
+
if (wrapper.current && !wrapper.current.contains(e.target)) {
|
|
36
|
+
setSuggestions([]);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
41
|
+
const query = e.target.value;
|
|
42
|
+
if (query) {
|
|
43
|
+
getSearchResults(query).then((suggestions) => {
|
|
44
|
+
setSuggestions(suggestions);
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
setSuggestions([]);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleClick = (index: number) => {
|
|
52
|
+
const display = getDisplayValue(suggestions[index]);
|
|
53
|
+
const value = getFieldValue(suggestions[index]);
|
|
54
|
+
searchBox.current.value = display;
|
|
55
|
+
onSuggestionSelected(name, value);
|
|
56
|
+
setSuggestions([]);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className={styles.autocomplete} ref={wrapper}>
|
|
61
|
+
<label className="cds--label">{labelText}</label>
|
|
62
|
+
<Layer>
|
|
63
|
+
<Search
|
|
64
|
+
id="autosuggest"
|
|
65
|
+
onChange={handleChange}
|
|
66
|
+
ref={searchBox}
|
|
67
|
+
className={styles.autocompleteSearch}
|
|
68
|
+
{...searchProps}
|
|
69
|
+
/>
|
|
70
|
+
</Layer>
|
|
71
|
+
{suggestions.length > 0 && (
|
|
72
|
+
<ul className={styles.suggestions}>
|
|
73
|
+
{suggestions.map((suggestion, index) => (
|
|
74
|
+
<li //eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
|
|
75
|
+
key={index}
|
|
76
|
+
onClick={(e) => handleClick(index)}>
|
|
77
|
+
{getDisplayValue(suggestion)}
|
|
78
|
+
</li>
|
|
79
|
+
))}
|
|
80
|
+
</ul>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.label01 {
|
|
6
|
+
@include type.type-style('label-01');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.suggestions {
|
|
10
|
+
position: relative;
|
|
11
|
+
border-top-width: 0;
|
|
12
|
+
list-style: none;
|
|
13
|
+
margin-top: 0;
|
|
14
|
+
max-height: 143px;
|
|
15
|
+
overflow-y: auto;
|
|
16
|
+
padding-left: 0;
|
|
17
|
+
width: 100%;
|
|
18
|
+
position: absolute;
|
|
19
|
+
left: 0;
|
|
20
|
+
background-color: #fff;
|
|
21
|
+
margin-bottom: 20px;
|
|
22
|
+
z-index: 99;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.suggestions li {
|
|
26
|
+
padding: spacing.$spacing-05;
|
|
27
|
+
line-height: 1.29;
|
|
28
|
+
color: #525252;
|
|
29
|
+
border-bottom: 1px solid #8d8d8d;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.suggestions li:hover {
|
|
33
|
+
background-color: #e5e5e5;
|
|
34
|
+
color: #161616;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.suggestions li:not(:last-of-type) {
|
|
39
|
+
border-bottom: 1px solid #999;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.autocomplete {
|
|
43
|
+
position: relative;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.autocompleteSearch {
|
|
47
|
+
width: 100%;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.suggestions a {
|
|
51
|
+
color: inherit;
|
|
52
|
+
text-decoration: none;
|
|
53
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Autosuggest } from './autosuggest.component';
|
|
3
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
4
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
5
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
6
|
+
import '@testing-library/jest-dom';
|
|
7
|
+
|
|
8
|
+
const mockPersons = [
|
|
9
|
+
{
|
|
10
|
+
uuid: 'randomuuid1',
|
|
11
|
+
display: 'John Doe',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
uuid: 'randomuuid2',
|
|
15
|
+
display: 'John Smith',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
uuid: 'randomuuid3',
|
|
19
|
+
display: 'James Smith',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
uuid: 'randomuuid4',
|
|
23
|
+
display: 'Spider Man',
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const mockGetSearchResults = async (query: string) => {
|
|
28
|
+
return mockPersons.filter((person) => {
|
|
29
|
+
return person.display.toUpperCase().includes(query.toUpperCase());
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleSuggestionSelected = jest.fn((field, value) => [field, value]);
|
|
34
|
+
|
|
35
|
+
describe('autosuggest', () => {
|
|
36
|
+
const setup = () => {
|
|
37
|
+
render(
|
|
38
|
+
<BrowserRouter>
|
|
39
|
+
<Autosuggest
|
|
40
|
+
labelText=""
|
|
41
|
+
name="person"
|
|
42
|
+
placeholder="Find Person"
|
|
43
|
+
onSuggestionSelected={handleSuggestionSelected}
|
|
44
|
+
getSearchResults={mockGetSearchResults}
|
|
45
|
+
getDisplayValue={(item) => item.display}
|
|
46
|
+
getFieldValue={(item) => item.uuid}
|
|
47
|
+
/>
|
|
48
|
+
</BrowserRouter>,
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
it('should render a search box', () => {
|
|
53
|
+
setup();
|
|
54
|
+
expect(screen.getByRole('searchbox')).toBeInTheDocument();
|
|
55
|
+
expect(screen.queryByRole('list')).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('shows search results in an ul', async () => {
|
|
59
|
+
setup();
|
|
60
|
+
const searchbox = screen.getByRole('searchbox');
|
|
61
|
+
fireEvent.change(searchbox, { target: { value: 'john' } });
|
|
62
|
+
const list = await waitFor(() => screen.getByRole('list'));
|
|
63
|
+
expect(list).toBeInTheDocument();
|
|
64
|
+
expect(list.children).toHaveLength(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('creates li items whose inner text is gotten through getDisplayValue', async () => {
|
|
68
|
+
setup();
|
|
69
|
+
const searchbox = screen.getByRole('searchbox');
|
|
70
|
+
fireEvent.change(searchbox, { target: { value: 'john' } });
|
|
71
|
+
const list = await waitFor(() => screen.getAllByRole('listitem'));
|
|
72
|
+
expect(list[0].textContent).toBe('John Doe');
|
|
73
|
+
expect(list[1].textContent).toBe('John Smith');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
xit('triggers onSuggestionSelected with correct values when li is clicked', async () => {
|
|
77
|
+
setup();
|
|
78
|
+
const searchbox = screen.getByRole('searchbox');
|
|
79
|
+
fireEvent.change(searchbox, { target: { value: 'john' } });
|
|
80
|
+
const listitems = await waitFor(() => screen.getAllByRole('listitem'));
|
|
81
|
+
fireEvent.click(listitems[0]);
|
|
82
|
+
expect(handleSuggestionSelected).toHaveBeenNthCalledWith(1, 'person', 'randomuuid1');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it.skip('sets search box value to selected suggestion', async () => {
|
|
86
|
+
setup();
|
|
87
|
+
let searchbox = screen.getByRole('searchbox');
|
|
88
|
+
fireEvent.change(searchbox, { target: { value: 'john' } });
|
|
89
|
+
const listitems = await waitFor(() => screen.getAllByRole('listitem'));
|
|
90
|
+
fireEvent.click(listitems[0]);
|
|
91
|
+
searchbox = screen.getByRole('searchbox');
|
|
92
|
+
expect(searchbox.textContent).toBe('John Doe');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
xit('clears suggestions when a suggestion is selected', async () => {
|
|
96
|
+
setup();
|
|
97
|
+
// screen.getByRole('x');
|
|
98
|
+
let list = screen.queryByRole('list');
|
|
99
|
+
expect(list).toBeNull();
|
|
100
|
+
const searchbox = screen.getByRole('searchbox');
|
|
101
|
+
fireEvent.change(searchbox, { target: { value: 'john' } });
|
|
102
|
+
list = await waitFor(() => screen.getByRole('list'));
|
|
103
|
+
expect(list).toBeInTheDocument();
|
|
104
|
+
const listitems = screen.getAllByRole('listitem');
|
|
105
|
+
fireEvent.click(listitems[0]);
|
|
106
|
+
list = screen.queryByRole('list');
|
|
107
|
+
expect(list).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
});
|
package/src/patient-registration/input/custom-input/estimated-age/estimated-age-input.component.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useField } from 'formik';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import { Input } from '../../basic-input/input/input.component';
|
|
6
|
+
import styles from './../../input.scss';
|
|
7
|
+
|
|
8
|
+
interface EstimatedAgeInputProps {
|
|
9
|
+
yearsName: string;
|
|
10
|
+
monthsName: string;
|
|
11
|
+
setBirthdate(field: string, value: any, shouldValidate?: boolean): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const EstimatedAgeInput: React.FC<EstimatedAgeInputProps> = ({ yearsName, monthsName, setBirthdate }) => {
|
|
15
|
+
const [yearsField] = useField(yearsName);
|
|
16
|
+
const [monthsField] = useField(monthsName);
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setBirthdate(
|
|
21
|
+
'birthdate',
|
|
22
|
+
dayjs().subtract(yearsField.value, 'year').subtract(monthsField.value, 'month').toISOString().split('T')[0],
|
|
23
|
+
);
|
|
24
|
+
}, [yearsField.value, monthsField.value, setBirthdate]);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<main className={styles.fieldRow}>
|
|
28
|
+
<Input id="number" labelText={t('years', 'Years')} name={yearsName} />
|
|
29
|
+
<Input id="number" labelText={t('months', 'Months')} name={monthsName} />
|
|
30
|
+
</main>
|
|
31
|
+
);
|
|
32
|
+
};
|
package/src/patient-registration/input/custom-input/estimated-age/estimated-age-input.test.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { Formik, Form } from 'formik';
|
|
4
|
+
import { EstimatedAgeInput } from './estimated-age-input.component';
|
|
5
|
+
|
|
6
|
+
describe.skip('estimated age input', () => {
|
|
7
|
+
const mockSetBirthdate = jest.fn();
|
|
8
|
+
|
|
9
|
+
const setupInput = async () => {
|
|
10
|
+
render(
|
|
11
|
+
<Formik initialValues={{ years: 0, months: 0 }} onSubmit={null}>
|
|
12
|
+
<Form>
|
|
13
|
+
<EstimatedAgeInput yearsName="years" monthsName="months" setBirthdate={mockSetBirthdate} />
|
|
14
|
+
</Form>
|
|
15
|
+
</Formik>,
|
|
16
|
+
);
|
|
17
|
+
const years = screen.getByLabelText('years') as HTMLInputElement;
|
|
18
|
+
const months = screen.getByLabelText('months') as HTMLInputElement;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
years,
|
|
22
|
+
months,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
it('exists', async () => {
|
|
27
|
+
const inputs = await setupInput();
|
|
28
|
+
expect(inputs.years.type).toEqual('number');
|
|
29
|
+
expect(inputs.months.type).toEqual('number');
|
|
30
|
+
});
|
|
31
|
+
it('calls setBirthdate', async () => {
|
|
32
|
+
mockSetBirthdate.mockReset();
|
|
33
|
+
await setupInput();
|
|
34
|
+
expect(mockSetBirthdate.mock.calls.length).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
});
|