@kenyaemr/esm-patient-registration-app 5.2.2 → 5.2.3

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.
Files changed (114) hide show
  1. package/dist/102.js +1 -0
  2. package/dist/102.js.map +1 -0
  3. package/dist/130.js +1 -1
  4. package/dist/130.js.map +1 -1
  5. package/dist/271.js +1 -0
  6. package/dist/319.js +1 -1
  7. package/dist/431.js +2 -0
  8. package/dist/431.js.map +1 -0
  9. package/dist/460.js +1 -1
  10. package/dist/537.js +1 -1
  11. package/dist/537.js.map +1 -1
  12. package/dist/574.js +1 -1
  13. package/dist/644.js +1 -0
  14. package/dist/757.js +1 -1
  15. package/dist/784.js +1 -1
  16. package/dist/788.js +1 -1
  17. package/dist/807.js +1 -1
  18. package/dist/833.js +1 -1
  19. package/dist/kenyaemr-esm-patient-registration-app.js +1 -0
  20. package/dist/{openmrs-esm-patient-registration-app.js.buildmanifest.json → kenyaemr-esm-patient-registration-app.js.buildmanifest.json} +97 -53
  21. package/dist/kenyaemr-esm-patient-registration-app.js.map +1 -0
  22. package/dist/main.js +1 -1
  23. package/dist/main.js.map +1 -1
  24. package/dist/routes.json +1 -1
  25. package/dist.tar.gz +0 -0
  26. package/package.json +4 -3
  27. package/src/add-patient-link.test.tsx +9 -7
  28. package/src/config-schema.ts +31 -38
  29. package/src/constants.ts +1 -1
  30. package/src/offline.resources.ts +2 -9
  31. package/src/offline.ts +2 -2
  32. package/src/patient-registration/before-save-prompt.tsx +2 -1
  33. package/src/patient-registration/field/__mocks__/field.resource.ts +1 -1
  34. package/src/patient-registration/field/address/address-field.component.tsx +5 -4
  35. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +2 -2
  36. package/src/patient-registration/field/address/address-search.component.tsx +1 -14
  37. package/src/patient-registration/field/address/custom-address-field.component.tsx +1 -1
  38. package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +3 -3
  39. package/src/patient-registration/field/address/tests/address-search-component.test.tsx +14 -7
  40. package/src/patient-registration/field/custom-field.component.tsx +1 -1
  41. package/src/patient-registration/field/dob/dob.component.tsx +2 -2
  42. package/src/patient-registration/field/dob/dob.test.tsx +0 -3
  43. package/src/patient-registration/field/field.component.tsx +4 -1
  44. package/src/patient-registration/field/field.resource.ts +2 -2
  45. package/src/patient-registration/field/field.test.tsx +98 -95
  46. package/src/patient-registration/field/gender/gender-field.component.tsx +4 -4
  47. package/src/patient-registration/field/gender/gender-field.test.tsx +7 -14
  48. package/src/patient-registration/field/id/id-field.component.tsx +6 -6
  49. package/src/patient-registration/field/id/id-field.test.tsx +9 -7
  50. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +1 -1
  51. package/src/patient-registration/field/name/name-field.component.tsx +1 -1
  52. package/src/patient-registration/field/obs/obs-field.component.tsx +35 -33
  53. package/src/patient-registration/field/obs/obs-field.test.tsx +106 -28
  54. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +1 -1
  55. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +67 -24
  56. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +54 -30
  57. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +4 -3
  58. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +1 -1
  59. package/src/patient-registration/field/person-attributes/{person-attributes.resource.tsx → person-attributes.resource.ts} +2 -2
  60. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +1 -1
  61. package/src/patient-registration/field/phone/phone-field.component.tsx +16 -0
  62. package/src/patient-registration/form-manager.test.ts +1 -1
  63. package/src/patient-registration/form-manager.ts +14 -22
  64. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +3 -3
  65. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +5 -5
  66. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +70 -58
  67. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +1 -1
  68. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +2 -5
  69. package/src/patient-registration/input/custom-input/identifier/utils.ts +1 -1
  70. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +1 -1
  71. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +6 -6
  72. package/src/patient-registration/patient-registration-context.ts +3 -4
  73. package/src/patient-registration/patient-registration-hooks.ts +12 -12
  74. package/src/patient-registration/patient-registration-utils.ts +7 -7
  75. package/src/patient-registration/patient-registration.component.tsx +19 -9
  76. package/src/patient-registration/{patient-registration.resource.tsx → patient-registration.resource.ts} +1 -1
  77. package/src/patient-registration/patient-registration.test.tsx +270 -251
  78. package/src/patient-registration/{patient-registration.types.tsx → patient-registration.types.ts} +11 -3
  79. package/src/patient-registration/section/death-info/death-info-section.test.tsx +33 -45
  80. package/src/patient-registration/section/demographics/demographics-section.test.tsx +1 -2
  81. package/src/patient-registration/section/generic-section.component.tsx +1 -1
  82. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +3 -3
  83. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +17 -5
  84. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +3 -3
  85. package/src/patient-registration/section/section-wrapper.component.tsx +1 -1
  86. package/src/patient-registration/section/section.component.tsx +1 -1
  87. package/src/patient-registration/validation/patient-registration-validation.test.tsx +140 -126
  88. package/src/patient-registration/validation/patient-registration-validation.tsx +54 -46
  89. package/src/routes.json +1 -0
  90. package/src/widgets/cancel-patient-edit.test.tsx +7 -4
  91. package/src/widgets/delete-identifier-confirmation-modal.test.tsx +7 -4
  92. package/src/widgets/display-photo.test.tsx +1 -1
  93. package/src/widgets/edit-patient-details-button.test.tsx +12 -7
  94. package/translations/am.json +22 -2
  95. package/translations/ar.json +22 -2
  96. package/translations/en.json +10 -10
  97. package/translations/es.json +22 -2
  98. package/translations/fr.json +22 -2
  99. package/translations/he.json +22 -2
  100. package/translations/km.json +22 -2
  101. package/translations/zh.json +89 -0
  102. package/translations/zh_CN.json +89 -0
  103. package/tsconfig.json +1 -1
  104. package/__mocks__/autogenerationoptions.mock.ts +0 -34
  105. package/dist/388.js +0 -2
  106. package/dist/388.js.map +0 -1
  107. package/dist/598.js +0 -1
  108. package/dist/598.js.map +0 -1
  109. package/dist/openmrs-esm-patient-registration-app.js +0 -1
  110. package/dist/openmrs-esm-patient-registration-app.js.map +0 -1
  111. package/src/patient-registration/field/__mocks__/identifier-types.mock.ts +0 -76
  112. package/src/patient-registration/field/__mocks__/identifiers.mock.ts +0 -27
  113. package/src/patient-registration/field/address/tests/mocks.ts +0 -98
  114. /package/dist/{388.js.LICENSE.txt → 431.js.LICENSE.txt} +0 -0
@@ -2,17 +2,86 @@ import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import { useConfig } from '@openmrs/esm-framework';
5
- import { FieldDefinition } from '../../../config-schema';
5
+ import { type FieldDefinition } from '../../../config-schema';
6
+ import { useConcept, useConceptAnswers } from '../field.resource';
6
7
  import { ObsField } from './obs-field.component';
7
8
 
8
9
  const mockUseConfig = useConfig as jest.Mock;
9
10
 
10
- // The UUIDs in this test all refer to ones that are in `__mocks__/field.resource.ts`
11
- jest.mock('../field.resource');
11
+ jest.mock('../field.resource'); // Mock the useConceptAnswers hook
12
+
13
+ const mockedUseConcept = useConcept as jest.Mock;
14
+ const mockedUseConceptAnswers = useConceptAnswers as jest.Mock;
15
+
16
+ const useConceptMockImpl = (uuid: string) => {
17
+ let data;
18
+ if (uuid == 'weight-uuid') {
19
+ data = {
20
+ uuid: 'weight-uuid',
21
+ display: 'Weight (kg)',
22
+ datatype: { display: 'Numeric', uuid: 'num' },
23
+ answers: [],
24
+ setMembers: [],
25
+ };
26
+ } else if (uuid == 'chief-complaint-uuid') {
27
+ data = {
28
+ uuid: 'chief-complaint-uuid',
29
+ display: 'Chief Complaint',
30
+ datatype: { display: 'Text', uuid: 'txt' },
31
+ answers: [],
32
+ setMembers: [],
33
+ };
34
+ } else if (uuid == 'nationality-uuid') {
35
+ data = {
36
+ uuid: 'nationality-uuid',
37
+ display: 'Nationality',
38
+ datatype: { display: 'Coded', uuid: 'cdd' },
39
+ answers: [
40
+ { display: 'USA', uuid: 'usa' },
41
+ { display: 'Mexico', uuid: 'mex' },
42
+ ],
43
+ setMembers: [],
44
+ };
45
+ } else {
46
+ throw Error(`Programming error, you probably didn't mean to do this: unknown concept uuid '${uuid}'`);
47
+ }
48
+ return {
49
+ data: data ?? null,
50
+ isLoading: !data,
51
+ };
52
+ };
53
+
54
+ const useConceptAnswersMockImpl = (uuid: string) => {
55
+ if (uuid == 'nationality-uuid') {
56
+ return {
57
+ data: [
58
+ { display: 'USA', uuid: 'usa' },
59
+ { display: 'Mexico', uuid: 'mex' },
60
+ ],
61
+ isLoading: false,
62
+ };
63
+ } else if (uuid == 'other-countries-uuid') {
64
+ return {
65
+ data: [
66
+ { display: 'Kenya', uuid: 'ke' },
67
+ { display: 'Uganda', uuid: 'ug' },
68
+ ],
69
+ isLoading: false,
70
+ };
71
+ } else if (uuid == '') {
72
+ return {
73
+ data: [],
74
+ isLoading: false,
75
+ };
76
+ } else {
77
+ throw Error(`Programming error, you probably didn't mean to do this: unknown concept answer set uuid '${uuid}'`);
78
+ }
79
+ };
12
80
 
13
81
  type FieldProps = {
14
82
  children: ({ field, form: { touched, errors } }) => React.ReactNode;
15
83
  };
84
+
16
85
  jest.mock('formik', () => ({
17
86
  ...(jest.requireActual('formik') as object),
18
87
  Field: jest.fn(({ children }: FieldProps) => <>{children({ field: {}, form: { touched: {}, errors: {} } })}</>),
@@ -24,18 +93,14 @@ const textFieldDef: FieldDefinition = {
24
93
  type: 'obs',
25
94
  label: '',
26
95
  placeholder: '',
96
+ showHeading: false,
27
97
  uuid: 'chief-complaint-uuid',
28
98
  validation: {
29
99
  required: false,
30
100
  matches: null,
31
101
  },
32
102
  answerConceptSetUuid: null,
33
- customConceptAnswers: [
34
- {
35
- uuid: 'concept-uuid',
36
- label: '',
37
- },
38
- ],
103
+ customConceptAnswers: [],
39
104
  };
40
105
 
41
106
  const numberFieldDef: FieldDefinition = {
@@ -43,18 +108,14 @@ const numberFieldDef: FieldDefinition = {
43
108
  type: 'obs',
44
109
  label: '',
45
110
  placeholder: '',
111
+ showHeading: false,
46
112
  uuid: 'weight-uuid',
47
113
  validation: {
48
114
  required: false,
49
115
  matches: null,
50
116
  },
51
117
  answerConceptSetUuid: null,
52
- customConceptAnswers: [
53
- {
54
- uuid: 'concept-uuid',
55
- label: '',
56
- },
57
- ],
118
+ customConceptAnswers: [],
58
119
  };
59
120
 
60
121
  const codedFieldDef: FieldDefinition = {
@@ -62,30 +123,30 @@ const codedFieldDef: FieldDefinition = {
62
123
  type: 'obs',
63
124
  label: '',
64
125
  placeholder: '',
126
+ showHeading: false,
65
127
  uuid: 'nationality-uuid',
66
128
  validation: {
67
129
  required: false,
68
130
  matches: null,
69
131
  },
70
132
  answerConceptSetUuid: null,
71
- customConceptAnswers: [
72
- {
73
- uuid: 'concept-uuid',
74
- label: 'Kenya',
75
- },
76
- ],
133
+ customConceptAnswers: [],
77
134
  };
78
135
 
79
136
  describe('ObsField', () => {
80
137
  beforeEach(() => {
81
138
  mockUseConfig.mockReturnValue({ registrationObs: { encounterTypeUuid: 'reg-enc-uuid' } });
139
+ mockedUseConcept.mockImplementation(useConceptMockImpl);
140
+ mockedUseConceptAnswers.mockImplementation(useConceptAnswersMockImpl);
82
141
  });
83
142
 
84
143
  it("logs an error and doesn't render if no registration encounter type is provided", () => {
85
144
  mockUseConfig.mockReturnValue({ registrationObs: { encounterTypeUuid: null } });
86
145
  console.error = jest.fn();
87
146
  render(<ObsField fieldDefinition={textFieldDef} />);
88
- expect(console.error).toHaveBeenCalledWith(expect.stringMatching(/no registration encounter type.*configure/i));
147
+ expect(console.error).toHaveBeenCalledWith(
148
+ expect.stringMatching(/no registration encounter type has been configured/i),
149
+ );
89
150
  expect(screen.queryByRole('textbox')).toBeNull();
90
151
  });
91
152
 
@@ -107,15 +168,10 @@ describe('ObsField', () => {
107
168
  // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
108
169
  const select = screen.getByRole('combobox');
109
170
  expect(select).toBeInTheDocument();
110
- expect(select).toHaveDisplayValue('');
171
+ expect(select).toHaveDisplayValue('Select an option');
111
172
  });
112
173
 
113
174
  it('select uses answerConcept for answers when it is provided', async () => {
114
- mockUseConfig.mockReturnValue({
115
- registrationObs: { encounterTypeUuid: 'reg-enc-uuid' },
116
- fieldDefinitions: [codedFieldDef],
117
- });
118
-
119
175
  const user = userEvent.setup();
120
176
 
121
177
  render(<ObsField fieldDefinition={{ ...codedFieldDef, answerConceptSetUuid: 'other-countries-uuid' }} />);
@@ -124,4 +180,26 @@ describe('ObsField', () => {
124
180
  expect(select).toBeInTheDocument();
125
181
  await user.selectOptions(select, 'Kenya');
126
182
  });
183
+
184
+ it('select uses customConceptAnswers for answers when provided', async () => {
185
+ const user = userEvent.setup();
186
+
187
+ render(
188
+ <ObsField
189
+ fieldDefinition={{
190
+ ...codedFieldDef,
191
+ customConceptAnswers: [
192
+ {
193
+ uuid: 'mozambique-uuid',
194
+ label: 'Mozambique',
195
+ },
196
+ ],
197
+ }}
198
+ />,
199
+ );
200
+ // expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
201
+ const select = screen.getByRole('combobox');
202
+ expect(select).toBeInTheDocument();
203
+ await user.selectOptions(select, 'Mozambique');
204
+ });
127
205
  });
@@ -3,7 +3,7 @@ import classNames from 'classnames';
3
3
  import { Layer, Select, SelectItem } from '@carbon/react';
4
4
  import { useConfig } from '@openmrs/esm-framework';
5
5
  import { Input } from '../../input/basic-input/input/input.component';
6
- import { CodedPersonAttributeConfig } from '../../patient-registration.types';
6
+ import { type CodedPersonAttributeConfig } from '../../patient-registration.types';
7
7
  import { useConceptAnswers } from '../field.resource';
8
8
  import { usePersonAttributeType } from './person-attributes.resource';
9
9
  import styles from './../field.scss';
@@ -1,18 +1,19 @@
1
- import React from 'react';
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { Field } from 'formik';
5
5
  import { Layer, Select, SelectItem } from '@carbon/react';
6
- import { Input } from '../../input/basic-input/input/input.component';
7
- import { PersonAttributeTypeResponse } from '../../patient-registration.types';
6
+ import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
8
7
  import { useConceptAnswers } from '../field.resource';
9
8
  import styles from './../field.scss';
9
+ import { reportError } from '@openmrs/esm-framework';
10
10
 
11
11
  export interface CodedPersonAttributeFieldProps {
12
12
  id: string;
13
13
  personAttributeType: PersonAttributeTypeResponse;
14
14
  answerConceptSetUuid: string;
15
15
  label?: string;
16
+ customConceptAnswers: Array<{ uuid: string; label?: string }>;
16
17
  }
17
18
 
18
19
  export function CodedPersonAttributeField({
@@ -20,14 +21,72 @@ export function CodedPersonAttributeField({
20
21
  personAttributeType,
21
22
  answerConceptSetUuid,
22
23
  label,
24
+ customConceptAnswers,
23
25
  }: CodedPersonAttributeFieldProps) {
24
- const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(answerConceptSetUuid);
26
+ const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(
27
+ customConceptAnswers.length ? '' : answerConceptSetUuid,
28
+ );
25
29
  const { t } = useTranslation();
26
30
  const fieldName = `attributes.${personAttributeType.uuid}`;
31
+ const [error, setError] = useState(false);
32
+
33
+ useEffect(() => {
34
+ if (!answerConceptSetUuid) {
35
+ reportError(
36
+ t(
37
+ 'codedPersonAttributeNoAnswerSet',
38
+ `The person attribute field '{{codedPersonAttributeFieldId}}' is of type 'coded' but has been defined without an answer concept set UUID. The 'answerConceptSetUuid' key is required.`,
39
+ { codedPersonAttributeFieldId: id },
40
+ ),
41
+ );
42
+ setError(true);
43
+ }
44
+ }, [answerConceptSetUuid]);
45
+
46
+ useEffect(() => {
47
+ if (!isLoadingConceptAnswers) {
48
+ if (!conceptAnswers) {
49
+ reportError(
50
+ t(
51
+ 'codedPersonAttributeAnswerSetInvalid',
52
+ `The coded person attribute field '{{codedPersonAttributeFieldId}}' has been defined with an invalid answer concept set UUID '{{answerConceptSetUuid}}'.`,
53
+ { codedPersonAttributeFieldId: id, answerConceptSetUuid },
54
+ ),
55
+ );
56
+ setError(true);
57
+ }
58
+ if (conceptAnswers?.length == 0) {
59
+ reportError(
60
+ t(
61
+ 'codedPersonAttributeAnswerSetEmpty',
62
+ `The coded person attribute field '{{codedPersonAttributeFieldId}}' has been defined with an answer concept set UUID '{{answerConceptSetUuid}}' that does not have any concept answers.`,
63
+ {
64
+ codedPersonAttributeFieldId: id,
65
+ answerConceptSetUuid,
66
+ },
67
+ ),
68
+ );
69
+ setError(true);
70
+ }
71
+ }
72
+ }, [isLoadingConceptAnswers, conceptAnswers]);
73
+
74
+ const answers = useMemo(() => {
75
+ if (customConceptAnswers.length) {
76
+ return customConceptAnswers;
77
+ }
78
+ return isLoadingConceptAnswers || !conceptAnswers
79
+ ? []
80
+ : conceptAnswers.map((answer) => ({ ...answer, label: answer.display }));
81
+ }, [customConceptAnswers, conceptAnswers, isLoadingConceptAnswers]);
82
+
83
+ if (error) {
84
+ return null;
85
+ }
27
86
 
28
87
  return (
29
88
  <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
30
- {!isLoadingConceptAnswers && conceptAnswers?.length ? (
89
+ {!isLoadingConceptAnswers ? (
31
90
  <Layer>
32
91
  <Field name={fieldName}>
33
92
  {({ field, form: { touched, errors }, meta }) => {
@@ -40,8 +99,8 @@ export function CodedPersonAttributeField({
40
99
  invalid={errors[fieldName] && touched[fieldName]}
41
100
  {...field}>
42
101
  <SelectItem value={''} text={t('selectAnOption', 'Select an option')} />
43
- {conceptAnswers.map((answer) => (
44
- <SelectItem key={answer.uuid} value={answer.uuid} text={answer.display} />
102
+ {answers.map((answer) => (
103
+ <SelectItem key={answer.uuid} value={answer.uuid} text={answer.label} />
45
104
  ))}
46
105
  </Select>
47
106
  </>
@@ -49,23 +108,7 @@ export function CodedPersonAttributeField({
49
108
  }}
50
109
  </Field>
51
110
  </Layer>
52
- ) : (
53
- <Layer>
54
- <Field name={fieldName}>
55
- {({ field, form: { touched, errors }, meta }) => {
56
- return (
57
- <Input
58
- id={id}
59
- name={`person-attribute-${personAttributeType.uuid}`}
60
- labelText={label ?? personAttributeType?.display}
61
- invalid={errors[fieldName] && touched[fieldName]}
62
- {...field}
63
- />
64
- );
65
- }}
66
- </Field>
67
- </Layer>
68
- )}
111
+ ) : null}
69
112
  </div>
70
113
  );
71
114
  }
@@ -21,6 +21,8 @@ describe('CodedPersonAttributeField', () => {
21
21
  format: 'org.openmrs.Concept',
22
22
  display: 'Referred by',
23
23
  uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
24
+ name: '',
25
+ description: '',
24
26
  };
25
27
  const answerConceptSetUuid = '6682d17f-0777-45e4-a39b-93f77eb3531c';
26
28
 
@@ -32,31 +34,47 @@ describe('CodedPersonAttributeField', () => {
32
34
  });
33
35
  });
34
36
 
35
- it('renders the Select component when conceptAnswers are available', () => {
36
- render(
37
- <Formik initialValues={{}} onSubmit={() => {}}>
38
- <Form>
39
- <CodedPersonAttributeField
40
- id="attributeId"
41
- personAttributeType={personAttributeType}
42
- answerConceptSetUuid={answerConceptSetUuid}
43
- label={personAttributeType.display}
44
- />
45
- </Form>
46
- </Formik>,
47
- );
48
-
49
- expect(screen.getByLabelText(/Referred by/i)).toBeInTheDocument();
50
- expect(screen.getByText(/Option 1/i)).toBeInTheDocument();
51
- expect(screen.getByText(/Option 2/i)).toBeInTheDocument();
37
+ it('shows error if there is no concept answer set provided', () => {
38
+ expect(() => {
39
+ render(
40
+ <Formik initialValues={{}} onSubmit={() => {}}>
41
+ <Form>
42
+ <CodedPersonAttributeField
43
+ id="attributeId"
44
+ personAttributeType={personAttributeType}
45
+ answerConceptSetUuid={null}
46
+ label={personAttributeType.display}
47
+ customConceptAnswers={[]}
48
+ />
49
+ </Form>
50
+ </Formik>,
51
+ );
52
+ }).toThrow(expect.stringMatching(/has been defined without an answer concept set UUID/i));
52
53
  });
53
54
 
54
- it('renders the Input component when conceptAnswers are not available', () => {
55
- mockedUseConceptAnswers.mockReturnValueOnce({
56
- data: null,
55
+ it('shows error if the concept answer set does not have any concept answers', () => {
56
+ mockedUseConceptAnswers.mockReturnValue({
57
+ data: [],
57
58
  isLoading: false,
58
59
  });
60
+ expect(() => {
61
+ render(
62
+ <Formik initialValues={{}} onSubmit={() => {}}>
63
+ <Form>
64
+ <CodedPersonAttributeField
65
+ id="attributeId"
66
+ personAttributeType={personAttributeType}
67
+ answerConceptSetUuid={answerConceptSetUuid}
68
+ label={personAttributeType.display}
69
+ customConceptAnswers={[]}
70
+ />
71
+ </Form>
72
+ </Formik>,
73
+ );
74
+ }).toThrow(expect.stringMatching(/does not have any concept answers/i));
75
+ });
59
76
 
77
+ it('renders the conceptAnswers as select options', () => {
60
78
  render(
61
79
  <Formik initialValues={{}} onSubmit={() => {}}>
62
80
  <Form>
@@ -65,23 +83,18 @@ describe('CodedPersonAttributeField', () => {
65
83
  personAttributeType={personAttributeType}
66
84
  answerConceptSetUuid={answerConceptSetUuid}
67
85
  label={personAttributeType.display}
86
+ customConceptAnswers={[]}
68
87
  />
69
88
  </Form>
70
89
  </Formik>,
71
90
  );
72
91
 
73
92
  expect(screen.getByLabelText(/Referred by/i)).toBeInTheDocument();
74
- expect(screen.queryByText(/Option 1/i)).not.toBeInTheDocument();
75
- expect(screen.queryByText(/Option 2/i)).not.toBeInTheDocument();
76
- expect(screen.getByRole('textbox')).toBeInTheDocument();
93
+ expect(screen.getByText(/Option 1/i)).toBeInTheDocument();
94
+ expect(screen.getByText(/Option 2/i)).toBeInTheDocument();
77
95
  });
78
96
 
79
- it('renders the Input component when conceptAnswers are still loading', () => {
80
- mockedUseConceptAnswers.mockReturnValueOnce({
81
- data: null,
82
- isLoading: true,
83
- });
84
-
97
+ it('renders customConceptAnswers as select options when they are provided', () => {
85
98
  render(
86
99
  <Formik initialValues={{}} onSubmit={() => {}}>
87
100
  <Form>
@@ -90,14 +103,25 @@ describe('CodedPersonAttributeField', () => {
90
103
  personAttributeType={personAttributeType}
91
104
  answerConceptSetUuid={answerConceptSetUuid}
92
105
  label={personAttributeType.display}
106
+ customConceptAnswers={[
107
+ {
108
+ uuid: 'A',
109
+ label: 'Special Option A',
110
+ },
111
+ {
112
+ uuid: 'B',
113
+ label: 'Special Option B',
114
+ },
115
+ ]}
93
116
  />
94
117
  </Form>
95
118
  </Formik>,
96
119
  );
97
120
 
98
121
  expect(screen.getByLabelText(/Referred by/i)).toBeInTheDocument();
122
+ expect(screen.getByText(/Special Option A/i)).toBeInTheDocument();
123
+ expect(screen.getByText(/Special Option B/i)).toBeInTheDocument();
99
124
  expect(screen.queryByText(/Option 1/i)).not.toBeInTheDocument();
100
125
  expect(screen.queryByText(/Option 2/i)).not.toBeInTheDocument();
101
- expect(screen.getByRole('textbox')).toBeInTheDocument();
102
126
  });
103
127
  });
@@ -1,6 +1,6 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { InlineNotification, TextInputSkeleton, SkeletonText } from '@carbon/react';
3
- import { FieldDefinition } from '../../../config-schema';
3
+ import { type FieldDefinition } from '../../../config-schema';
4
4
  import { CodedPersonAttributeField } from './coded-person-attribute-field.component';
5
5
  import { usePersonAttributeType } from './person-attributes.resource';
6
6
  import { TextPersonAttributeField } from './text-person-attribute-field.component';
@@ -24,9 +24,9 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
24
24
  return (
25
25
  <TextPersonAttributeField
26
26
  personAttributeType={personAttributeType}
27
- validationRegex={fieldDefinition.validation.matches}
27
+ validationRegex={fieldDefinition.validation?.matches ?? ''}
28
28
  label={fieldDefinition.label}
29
- required={fieldDefinition.validation.required}
29
+ required={fieldDefinition.validation?.required ?? false}
30
30
  id={fieldDefinition?.id}
31
31
  />
32
32
  );
@@ -37,6 +37,7 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
37
37
  answerConceptSetUuid={fieldDefinition.answerConceptSetUuid}
38
38
  label={fieldDefinition.label}
39
39
  id={fieldDefinition?.id}
40
+ customConceptAnswers={fieldDefinition.customConceptAnswers ?? []}
40
41
  />
41
42
  );
42
43
  default:
@@ -4,7 +4,7 @@ import { usePersonAttributeType } from './person-attributes.resource';
4
4
  import { PersonAttributeField } from './person-attribute-field.component';
5
5
  import { useConceptAnswers } from '../field.resource';
6
6
  import { Form, Formik } from 'formik';
7
- import { FieldDefinition } from '../../../config-schema';
7
+ import { type FieldDefinition } from '../../../config-schema';
8
8
 
9
9
  jest.mock('./person-attributes.resource'); // Mock the usePersonAttributeType hook
10
10
  jest.mock('../field.resource'); // Mock the useConceptAnswers hook
@@ -1,6 +1,6 @@
1
- import { FetchResponse, openmrsFetch } from '@openmrs/esm-framework';
1
+ import { type FetchResponse, openmrsFetch } from '@openmrs/esm-framework';
2
2
  import useSWRImmutable from 'swr/immutable';
3
- import { PersonAttributeTypeResponse } from '../../patient-registration.types';
3
+ import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
4
4
 
5
5
  export function usePersonAttributeType(personAttributeTypeUuid: string): {
6
6
  data: PersonAttributeTypeResponse;
@@ -3,7 +3,7 @@ import classNames from 'classnames';
3
3
  import { Field } from 'formik';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Input } from '../../input/basic-input/input/input.component';
6
- import { PersonAttributeTypeResponse } from '../../patient-registration.types';
6
+ import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
7
7
  import styles from './../field.scss';
8
8
 
9
9
  export interface TextPersonAttributeFieldProps {
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { PersonAttributeField } from '../person-attributes/person-attribute-field.component';
3
+ import { useConfig } from '@openmrs/esm-framework';
4
+ import { type RegistrationConfig } from '../../../config-schema';
5
+
6
+ export function PhoneField() {
7
+ const config = useConfig<RegistrationConfig>();
8
+
9
+ const fieldDefinition = {
10
+ id: 'phone',
11
+ type: 'person attribute',
12
+ uuid: config.fieldConfigurations.phone.personAttributeUuid,
13
+ showHeading: false,
14
+ };
15
+ return <PersonAttributeField fieldDefinition={fieldDefinition} />;
16
+ }
@@ -1,5 +1,5 @@
1
1
  import { FormManager } from './form-manager';
2
- import { FormValues } from './patient-registration.types';
2
+ import { type FormValues } from './patient-registration.types';
3
3
 
4
4
  jest.mock('./patient-registration.resource');
5
5
 
@@ -1,15 +1,15 @@
1
- import { FetchResponse, openmrsFetch, queueSynchronizationItem, Session } from '@openmrs/esm-framework';
1
+ import { type FetchResponse, openmrsFetch, queueSynchronizationItem, type Session } from '@openmrs/esm-framework';
2
2
  import { patientRegistration } from '../constants';
3
3
  import {
4
- FormValues,
5
- AttributeValue,
6
- PatientUuidMapType,
7
- Patient,
8
- CapturePhotoProps,
9
- PatientIdentifier,
10
- PatientRegistration,
11
- RelationshipValue,
12
- Encounter,
4
+ type FormValues,
5
+ type AttributeValue,
6
+ type PatientUuidMapType,
7
+ type Patient,
8
+ type CapturePhotoProps,
9
+ type PatientIdentifier,
10
+ type PatientRegistration,
11
+ type RelationshipValue,
12
+ type Encounter,
13
13
  } from './patient-registration.types';
14
14
  import {
15
15
  addPatientIdentifier,
@@ -24,7 +24,7 @@ import {
24
24
  updatePatientIdentifier,
25
25
  saveEncounter,
26
26
  } from './patient-registration.resource';
27
- import { RegistrationConfig } from '../config-schema';
27
+ import { type RegistrationConfig } from '../config-schema';
28
28
 
29
29
  export type SavePatientForm = (
30
30
  isNewPatient: boolean,
@@ -252,7 +252,7 @@ export class FormManager {
252
252
  });
253
253
 
254
254
  /*
255
- If there was initially an identifier assigned to the patient,
255
+ If there was initially an identifier assigned to the patient,
256
256
  which is now not present in the patientIdentifiers(values.identifiers),
257
257
  this means that the identifier is meant to be deleted, hence we need
258
258
  to delete the respective identifiers.
@@ -302,7 +302,7 @@ export class FormManager {
302
302
  person: {
303
303
  uuid: values.patientUuid,
304
304
  names: FormManager.getNames(values, patientUuidMap),
305
- gender: values.gender.charAt(0),
305
+ gender: values.gender.charAt(0).toUpperCase(),
306
306
  birthdate,
307
307
  birthdateEstimated: values.birthdateEstimated,
308
308
  attributes: FormManager.getPatientAttributes(isNewPatient, values, patientUuidMap),
@@ -383,21 +383,13 @@ export class FormManager {
383
383
  // provide a valid fhir.Patient object. The various patient chart modules should be able to handle
384
384
  // such missing props correctly (and should be updated if they don't).
385
385
 
386
- // Gender in the original object only uses a single letter. fhir.Patient expects a full string.
387
- const genderMap = {
388
- M: 'male',
389
- F: 'female',
390
- O: 'other',
391
- U: 'unknown',
392
- };
393
-
394
386
  // Mapping inspired by:
395
387
  // https://github.com/openmrs/openmrs-module-fhir/blob/669b3c52220bb9abc622f815f4dc0d8523687a57/api/src/main/java/org/openmrs/module/fhir/api/util/FHIRPatientUtil.java#L36
396
388
  // https://github.com/openmrs/openmrs-esm-patient-management/blob/94e6f637fb37cf4984163c355c5981ea6b8ca38c/packages/esm-patient-search-app/src/patient-search-result/patient-search-result.component.tsx#L21
397
389
  // Update as required.
398
390
  return {
399
391
  id: patient.uuid,
400
- gender: genderMap[patient.person?.gender],
392
+ gender: patient.person?.gender,
401
393
  birthDate: patient.person?.birthdate,
402
394
  deceasedBoolean: patient.person.dead,
403
395
  deceasedDateTime: patient.person.deathDate,
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { render, waitFor, screen } from '@testing-library/react';
2
+ import { render, screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import { Formik, Form } from 'formik';
5
5
  import { SelectInput } from './select-input.component';
@@ -28,7 +28,7 @@ describe('the select input', () => {
28
28
 
29
29
  await user.selectOptions(input, expected);
30
30
 
31
- await waitFor(() => expect(input.value).toEqual(expected));
31
+ await expect(input.value).toEqual(expected);
32
32
  });
33
33
 
34
34
  it('should show optional label if the input is not required', async () => {
@@ -40,7 +40,7 @@ describe('the select input', () => {
40
40
  </Formik>,
41
41
  );
42
42
 
43
- await waitFor(() => expect(screen.findByRole('combobox')));
43
+ await expect(screen.findByRole('combobox'));
44
44
 
45
45
  const selectInput = screen.getByRole('combobox', { name: 'Select (optional)' }) as HTMLSelectElement;
46
46
  expect(selectInput.labels).toHaveLength(1);