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

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 (108) hide show
  1. package/dist/130.js +1 -1
  2. package/dist/130.js.LICENSE.txt +0 -2
  3. package/dist/130.js.map +1 -1
  4. package/dist/152.js +1 -0
  5. package/dist/152.js.map +1 -0
  6. package/dist/255.js +2 -0
  7. package/dist/255.js.map +1 -0
  8. package/dist/303.js +1 -0
  9. package/dist/303.js.map +1 -0
  10. package/dist/319.js +1 -1
  11. package/dist/330.js +1 -1
  12. package/dist/388.js +2 -0
  13. package/dist/388.js.LICENSE.txt +34 -0
  14. package/dist/388.js.map +1 -0
  15. package/dist/460.js +1 -1
  16. package/dist/537.js +1 -1
  17. package/dist/537.js.map +1 -1
  18. package/dist/574.js +1 -1
  19. package/dist/59.js +1 -0
  20. package/dist/59.js.map +1 -0
  21. package/dist/598.js +1 -0
  22. package/dist/598.js.map +1 -0
  23. package/dist/62.js +1 -1
  24. package/dist/62.js.map +1 -1
  25. package/dist/729.js +1 -0
  26. package/dist/729.js.map +1 -0
  27. package/dist/735.js +1 -1
  28. package/dist/757.js +1 -1
  29. package/dist/788.js +1 -1
  30. package/dist/807.js +1 -1
  31. package/dist/833.js +1 -1
  32. package/dist/840.js +1 -0
  33. package/dist/840.js.map +1 -0
  34. package/dist/main.js +1 -1
  35. package/dist/main.js.LICENSE.txt +35 -0
  36. package/dist/main.js.map +1 -1
  37. package/dist/openmrs-esm-patient-registration-app.js +1 -1
  38. package/dist/openmrs-esm-patient-registration-app.js.buildmanifest.json +114 -221
  39. package/dist/openmrs-esm-patient-registration-app.js.map +1 -1
  40. package/dist/routes.json +1 -1
  41. package/package.json +4 -3
  42. package/src/declarations.d.ts +2 -0
  43. package/src/index.ts +23 -14
  44. package/src/patient-registration/field/address/address-field.component.tsx +2 -2
  45. package/src/patient-registration/field/address/custom-address-field.component.tsx +3 -2
  46. package/src/patient-registration/field/dob/dob.component.tsx +51 -42
  47. package/src/patient-registration/field/dob/dob.test.tsx +1 -14
  48. package/src/patient-registration/field/field.resource.ts +5 -5
  49. package/src/patient-registration/field/id/id-field.component.tsx +8 -6
  50. package/src/patient-registration/field/id/{identifier-selection-overlay.tsx → identifier-selection-overlay.component.tsx} +27 -23
  51. package/src/patient-registration/field/obs/obs-field.component.tsx +4 -3
  52. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +2 -1
  53. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +4 -3
  54. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +8 -2
  55. package/src/patient-registration/field/person-attributes/person-attributes.resource.tsx +1 -1
  56. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +3 -2
  57. package/src/patient-registration/input/basic-input/input/input.component.tsx +122 -2
  58. package/src/patient-registration/input/combo-input/combo-input.component.tsx +7 -6
  59. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +109 -6
  60. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +9 -0
  61. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +2 -2
  62. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +2 -1
  63. package/src/patient-registration/patient-registration.component.tsx +49 -44
  64. package/src/patient-registration/patient-registration.resource.tsx +18 -7
  65. package/src/patient-registration/patient-registration.scss +6 -0
  66. package/src/patient-registration/patient-registration.test.tsx +19 -21
  67. package/src/patient-registration/section/death-info/death-info-section.component.tsx +2 -1
  68. package/src/patient-registration/section/demographics/demographics-section.test.tsx +1 -14
  69. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +27 -20
  70. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +1 -1
  71. package/src/patient-registration/validation/patient-registration-validation.tsx +6 -0
  72. package/src/root.component.tsx +3 -2
  73. package/src/routes.json +7 -0
  74. package/translations/am.json +18 -7
  75. package/translations/ar.json +17 -6
  76. package/translations/en.json +16 -5
  77. package/translations/es.json +17 -6
  78. package/translations/fr.json +18 -7
  79. package/translations/he.json +28 -17
  80. package/translations/km.json +30 -19
  81. package/dist/117.js +0 -2
  82. package/dist/117.js.map +0 -1
  83. package/dist/218.js +0 -1
  84. package/dist/218.js.map +0 -1
  85. package/dist/275.js +0 -1
  86. package/dist/275.js.map +0 -1
  87. package/dist/317.js +0 -2
  88. package/dist/317.js.LICENSE.txt +0 -6
  89. package/dist/317.js.map +0 -1
  90. package/dist/348.js +0 -1
  91. package/dist/348.js.map +0 -1
  92. package/dist/520.js +0 -2
  93. package/dist/520.js.LICENSE.txt +0 -14
  94. package/dist/520.js.map +0 -1
  95. package/dist/635.js +0 -1
  96. package/dist/635.js.map +0 -1
  97. package/dist/68.js +0 -1
  98. package/dist/68.js.map +0 -1
  99. package/dist/693.js +0 -1
  100. package/dist/693.js.map +0 -1
  101. package/dist/742.js +0 -1
  102. package/dist/742.js.map +0 -1
  103. package/dist/857.js +0 -1
  104. package/dist/857.js.map +0 -1
  105. package/dist/975.js +0 -1
  106. package/dist/975.js.map +0 -1
  107. /package/dist/{117.js.LICENSE.txt → 255.js.LICENSE.txt} +0 -0
  108. /package/src/patient-registration/ui-components/overlay/{index.tsx → overlay.component.tsx} +0 -0
@@ -42,7 +42,13 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
42
42
  default:
43
43
  return (
44
44
  <InlineNotification kind="error" title="Error">
45
- Patient attribute type has unknown format "{personAttributeType.format}"
45
+ {t(
46
+ 'unknownPatientAttributeType',
47
+ 'Patient attribute type has unknown format {{personAttributeTypeFormat}}',
48
+ {
49
+ personAttributeTypeFormat: personAttributeType.format,
50
+ },
51
+ )}
46
52
  </InlineNotification>
47
53
  );
48
54
  }
@@ -62,7 +68,7 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
62
68
  <div>
63
69
  {fieldDefinition.showHeading && <h4 className={styles.productiveHeading02Light}>{fieldDefinition?.label}</h4>}
64
70
  <InlineNotification kind="error" title={t('error', 'Error')}>
65
- {t('unableToFetch', 'Unable to fetch person attribute type - {personattributetype}', {
71
+ {t('unableToFetch', 'Unable to fetch person attribute type - {{personattributetype}}', {
66
72
  personattributetype: fieldDefinition?.label ?? fieldDefinition?.id,
67
73
  })}
68
74
  </InlineNotification>
@@ -1,4 +1,4 @@
1
- import { FetchResponse, openmrsFetch, showToast } from '@openmrs/esm-framework';
1
+ import { FetchResponse, openmrsFetch } from '@openmrs/esm-framework';
2
2
  import useSWRImmutable from 'swr/immutable';
3
3
  import { PersonAttributeTypeResponse } from '../../patient-registration.types';
4
4
 
@@ -1,9 +1,10 @@
1
1
  import React from 'react';
2
+ import classNames from 'classnames';
2
3
  import { Field } from 'formik';
3
4
  import { useTranslation } from 'react-i18next';
4
5
  import { Input } from '../../input/basic-input/input/input.component';
5
- import styles from './../field.scss';
6
6
  import { PersonAttributeTypeResponse } from '../../patient-registration.types';
7
+ import styles from './../field.scss';
7
8
 
8
9
  export interface TextPersonAttributeFieldProps {
9
10
  id: string;
@@ -37,7 +38,7 @@ export function TextPersonAttributeField({
37
38
  const fieldName = `attributes.${personAttributeType.uuid}`;
38
39
 
39
40
  return (
40
- <div className={`${styles.customField} ${styles.halfWidthInDesktopView}`}>
41
+ <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
41
42
  <Field name={fieldName} validate={validateInput}>
42
43
  {({ field, form: { touched, errors }, meta }) => {
43
44
  return (
@@ -1,13 +1,133 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { Layer, TextInput, TextInputProps } from '@carbon/react';
3
+ import { Layer, TextInput } from '@carbon/react';
4
4
  import { useField } from 'formik';
5
5
 
6
+ // FIXME Temporarily imported here
7
+ export interface TextInputProps
8
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'defaultValue' | 'id' | 'size' | 'value'> {
9
+ /**
10
+ * Specify an optional className to be applied to the `<input>` node
11
+ */
12
+ className?: string;
13
+
14
+ /**
15
+ * Optionally provide the default value of the `<input>`
16
+ */
17
+ defaultValue?: string | number;
18
+
19
+ /**
20
+ * Specify whether the `<input>` should be disabled
21
+ */
22
+ disabled?: boolean;
23
+
24
+ /**
25
+ * Specify whether to display the character counter
26
+ */
27
+ enableCounter?: boolean;
28
+
29
+ /**
30
+ * Provide text that is used alongside the control label for additional help
31
+ */
32
+ helperText?: React.ReactNode;
33
+
34
+ /**
35
+ * Specify whether you want the underlying label to be visually hidden
36
+ */
37
+ hideLabel?: boolean;
38
+
39
+ /**
40
+ * Specify a custom `id` for the `<input>`
41
+ */
42
+ id: string;
43
+
44
+ /**
45
+ * `true` to use the inline version.
46
+ */
47
+ inline?: boolean;
48
+
49
+ /**
50
+ * Specify whether the control is currently invalid
51
+ */
52
+ invalid?: boolean;
53
+
54
+ /**
55
+ * Provide the text that is displayed when the control is in an invalid state
56
+ */
57
+ invalidText?: React.ReactNode;
58
+
59
+ /**
60
+ * Provide the text that will be read by a screen reader when visiting this
61
+ * control
62
+ */
63
+ labelText: React.ReactNode;
64
+
65
+ /**
66
+ * `true` to use the light version. For use on $ui-01 backgrounds only.
67
+ * Don't use this to make tile background color same as container background color.
68
+ * 'The `light` prop for `TextInput` has ' +
69
+ 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'
70
+ */
71
+ light?: boolean;
72
+
73
+ /**
74
+ * Max character count allowed for the input. This is needed in order for enableCounter to display
75
+ */
76
+ maxCount?: number;
77
+
78
+ /**
79
+ * Optionally provide an `onChange` handler that is called whenever `<input>`
80
+ * is updated
81
+ */
82
+ onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
83
+
84
+ /**
85
+ * Optionally provide an `onClick` handler that is called whenever the
86
+ * `<input>` is clicked
87
+ */
88
+ onClick?: (event: React.MouseEvent<HTMLElement>) => void;
89
+
90
+ /**
91
+ * Specify the placeholder attribute for the `<input>`
92
+ */
93
+ placeholder?: string;
94
+
95
+ /**
96
+ * Whether the input should be read-only
97
+ */
98
+ readOnly?: boolean;
99
+
100
+ /**
101
+ * Specify the size of the Text Input. Currently supports the following:
102
+ */
103
+ size?: 'sm' | 'md' | 'lg' | 'xl';
104
+
105
+ /**
106
+ * Specify the type of the `<input>`
107
+ */
108
+ type?: string;
109
+
110
+ /**
111
+ * Specify the value of the `<input>`
112
+ */
113
+ value?: string | number | undefined;
114
+
115
+ /**
116
+ * Specify whether the control is currently in warning state
117
+ */
118
+ warn?: boolean;
119
+
120
+ /**
121
+ * Provide the text that is displayed when the control is in warning state
122
+ */
123
+ warnText?: React.ReactNode;
124
+ }
125
+
6
126
  interface InputProps extends TextInputProps {
7
127
  checkWarning?(value: string): string;
8
128
  }
9
129
 
10
- export const Input: React.FC<any> = ({ checkWarning, ...props }) => {
130
+ export const Input: React.FC<InputProps> = ({ checkWarning, ...props }) => {
11
131
  const [field, meta] = useField(props.name);
12
132
  const { t } = useTranslation();
13
133
 
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import classNames from 'classnames';
2
3
  import { TextInput, Layer } from '@carbon/react';
3
4
  import SelectionTick from './selection-tick.component';
4
5
  import styles from '../input.scss';
@@ -98,19 +99,19 @@ const ComboInput: React.FC<ComboInputProps> = ({ entries, fieldProps, handleInpu
98
99
  <div id="downshift-1-menu" className="cds--list-box__menu" role="listbox">
99
100
  {filteredEntries.map((entry, indx) => (
100
101
  <div
102
+ className={classNames('cds--list-box__menu-item', {
103
+ 'cds--list-box__menu-item--highlighted': indx === highlightedEntry,
104
+ })}
101
105
  key={indx}
102
106
  id="downshift-1-item-0"
103
107
  role="option"
104
- className={`cds--list-box__menu-item ${
105
- indx === highlightedEntry && 'cds--list-box__menu-item--highlighted'
106
- }`}
107
108
  tabIndex={-1}
108
109
  aria-selected="true"
109
110
  onClick={() => handleOptionClick(entry)}>
110
111
  <div
111
- className={`cds--list-box__menu-item__option ${styles.comboInputItemOption} ${
112
- entry === value && 'cds--list-box__menu-item--active'
113
- }`}>
112
+ className={classNames('cds--list-box__menu-item__option', styles.comboInputItemOption, {
113
+ 'cds--list-box__menu-item--active': entry === value,
114
+ })}>
114
115
  {entry}
115
116
  {entry === value && <SelectionTick />}
116
117
  </div>
@@ -1,27 +1,123 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { useTranslation } from 'react-i18next';
1
+ import React, { HTMLAttributes, useEffect, useRef, useState } from 'react';
3
2
  import { Layer, Search, SearchProps } from '@carbon/react';
3
+ import classNames from 'classnames';
4
4
  import styles from './autosuggest.scss';
5
5
 
6
+ // FIXME Temporarily included types from Carbon
7
+ type InputPropsBase = Omit<HTMLAttributes<HTMLInputElement>, 'onChange'>;
8
+ interface SearchProps extends InputPropsBase {
9
+ /**
10
+ * Specify an optional value for the `autocomplete` property on the underlying
11
+ * `<input>`, defaults to "off"
12
+ */
13
+ autoComplete?: string;
14
+
15
+ /**
16
+ * Specify an optional className to be applied to the container node
17
+ */
18
+ className?: string;
19
+
20
+ /**
21
+ * Specify a label to be read by screen readers on the "close" button
22
+ */
23
+ closeButtonLabelText?: string;
24
+
25
+ /**
26
+ * Optionally provide the default value of the `<input>`
27
+ */
28
+ defaultValue?: string | number;
29
+
30
+ /**
31
+ * Specify whether the `<input>` should be disabled
32
+ */
33
+ disabled?: boolean;
34
+
35
+ /**
36
+ * Specify whether or not ExpandableSearch should render expanded or not
37
+ */
38
+ isExpanded?: boolean;
39
+
40
+ /**
41
+ * Specify a custom `id` for the input
42
+ */
43
+ id?: string;
44
+
45
+ /**
46
+ * Provide the label text for the Search icon
47
+ */
48
+ labelText: React.ReactNode;
49
+
50
+ /**
51
+ * Optional callback called when the search value changes.
52
+ */
53
+ onChange?(e: { target: HTMLInputElement; type: 'change' }): void;
54
+
55
+ /**
56
+ * Optional callback called when the search value is cleared.
57
+ */
58
+ onClear?(): void;
59
+
60
+ /**
61
+ * Optional callback called when the magnifier icon is clicked in ExpandableSearch.
62
+ */
63
+ onExpand?(e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void;
64
+
65
+ /**
66
+ * Provide an optional placeholder text for the Search.
67
+ * Note: if the label and placeholder differ,
68
+ * VoiceOver on Mac will read both
69
+ */
70
+ placeholder?: string;
71
+
72
+ /**
73
+ * Rendered icon for the Search.
74
+ * Can be a React component class
75
+ */
76
+ renderIcon?: React.ComponentType | React.FunctionComponent;
77
+
78
+ /**
79
+ * Specify the role for the underlying `<input>`, defaults to `searchbox`
80
+ */
81
+ role?: string;
82
+
83
+ /**
84
+ * Specify the size of the Search
85
+ */
86
+ size?: 'sm' | 'md' | 'lg';
87
+
88
+ /**
89
+ * Optional prop to specify the type of the `<input>`
90
+ */
91
+ type?: string;
92
+
93
+ /**
94
+ * Specify the value of the `<input>`
95
+ */
96
+ value?: string | number;
97
+ }
98
+
6
99
  interface AutosuggestProps extends SearchProps {
7
100
  getDisplayValue: Function;
8
101
  getFieldValue: Function;
9
102
  getSearchResults: (query: string) => Promise<any>;
10
103
  onSuggestionSelected: (field: string, value: string) => void;
104
+ invalid?: boolean | undefined;
105
+ invalidText?: string | undefined;
11
106
  }
12
107
 
13
- export const Autosuggest: React.FC<any> = ({
108
+ export const Autosuggest: React.FC<AutosuggestProps> = ({
14
109
  getDisplayValue,
15
110
  getFieldValue,
16
111
  getSearchResults,
17
112
  onSuggestionSelected,
113
+ invalid,
114
+ invalidText,
18
115
  ...searchProps
19
116
  }) => {
20
117
  const [suggestions, setSuggestions] = useState([]);
21
118
  const searchBox = useRef(null);
22
119
  const wrapper = useRef(null);
23
- const { t } = useTranslation();
24
- const { name, labelText } = searchProps;
120
+ const { id: name, labelText } = searchProps;
25
121
 
26
122
  useEffect(() => {
27
123
  document.addEventListener('mousedown', handleClickOutsideComponent);
@@ -39,6 +135,7 @@ export const Autosuggest: React.FC<any> = ({
39
135
 
40
136
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
41
137
  const query = e.target.value;
138
+ onSuggestionSelected(name, undefined);
42
139
  if (query) {
43
140
  getSearchResults(query).then((suggestions) => {
44
141
  setSuggestions(suggestions);
@@ -48,6 +145,10 @@ export const Autosuggest: React.FC<any> = ({
48
145
  }
49
146
  };
50
147
 
148
+ const handleClear = (e: React.ChangeEvent<HTMLInputElement>) => {
149
+ onSuggestionSelected(name, undefined);
150
+ };
151
+
51
152
  const handleClick = (index: number) => {
52
153
  const display = getDisplayValue(suggestions[index]);
53
154
  const value = getFieldValue(suggestions[index]);
@@ -59,10 +160,11 @@ export const Autosuggest: React.FC<any> = ({
59
160
  return (
60
161
  <div className={styles.autocomplete} ref={wrapper}>
61
162
  <label className="cds--label">{labelText}</label>
62
- <Layer>
163
+ <Layer className={classNames({ [styles.invalid]: invalid })}>
63
164
  <Search
64
165
  id="autosuggest"
65
166
  onChange={handleChange}
167
+ onClear={handleClear}
66
168
  ref={searchBox}
67
169
  className={styles.autocompleteSearch}
68
170
  {...searchProps}
@@ -79,6 +181,7 @@ export const Autosuggest: React.FC<any> = ({
79
181
  ))}
80
182
  </ul>
81
183
  )}
184
+ {invalid ? <label className={classNames(styles.invalidMsg)}>{invalidText}</label> : <></>}
82
185
  </div>
83
186
  );
84
187
  };
@@ -51,3 +51,12 @@
51
51
  color: inherit;
52
52
  text-decoration: none;
53
53
  }
54
+
55
+ .invalid input {
56
+ outline: 2px solid var(--cds-support-error, #da1e28);
57
+ outline-offset: -2px;
58
+ }
59
+
60
+ .invalidMsg {
61
+ color: var(--cds-text-error, #da1e28);
62
+ }
@@ -38,7 +38,7 @@ describe('autosuggest', () => {
38
38
  <BrowserRouter>
39
39
  <Autosuggest
40
40
  labelText=""
41
- name="person"
41
+ id="person"
42
42
  placeholder="Find Person"
43
43
  onSuggestionSelected={handleSuggestionSelected}
44
44
  getSearchResults={mockGetSearchResults}
@@ -79,7 +79,7 @@ describe('autosuggest', () => {
79
79
  fireEvent.change(searchbox, { target: { value: 'john' } });
80
80
  const listitems = await waitFor(() => screen.getAllByRole('listitem'));
81
81
  fireEvent.click(listitems[0]);
82
- expect(handleSuggestionSelected).toHaveBeenNthCalledWith(1, 'person', 'randomuuid1');
82
+ expect(handleSuggestionSelected).toHaveBeenNthCalledWith(4, 'person', 'randomuuid1');
83
83
  });
84
84
 
85
85
  it('should clear the suggestions when a suggestion is selected', async () => {
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import classNames from 'classnames';
2
3
  import { v4 } from 'uuid';
3
4
  import { FormValues } from '../../patient-registration.types';
4
5
  import styles from './../input.scss';
@@ -41,8 +42,8 @@ export const DummyDataInput: React.FC<DummyDataInputProps> = ({ setValues }) =>
41
42
  return (
42
43
  <main>
43
44
  <button
45
+ className={classNames('omrs-btn omrs-filled-neutral', styles.dummyData)}
44
46
  onClick={() => setValues(dummyFormValues)}
45
- className={`omrs-btn omrs-filled-neutral ${styles.dummyData}`}
46
47
  type="button"
47
48
  aria-label="Dummy Data Input">
48
49
  Input Dummy Data
@@ -1,28 +1,19 @@
1
1
  import React, { useState, useEffect, useContext, useMemo, useRef } from 'react';
2
- import { Button, Link } from '@carbon/react';
2
+ import classNames from 'classnames';
3
+ import { Button, Link, InlineLoading } from '@carbon/react';
3
4
  import { XAxis, ShareKnowledge } from '@carbon/react/icons';
4
- import { Router, useLocation, useParams } from 'react-router-dom';
5
+ import { useLocation, useParams } from 'react-router-dom';
5
6
  import { useTranslation } from 'react-i18next';
6
7
  import { Formik, Form, FormikHelpers } from 'formik';
7
- import { createErrorHandler, showToast, useConfig, interpolateUrl, usePatient } from '@openmrs/esm-framework';
8
+ import { createErrorHandler, showSnackbar, useConfig, interpolateUrl, usePatient } from '@openmrs/esm-framework';
8
9
  import { validationSchema as initialSchema } from './validation/patient-registration-validation';
9
- import { FormValues, CapturePhotoProps, PatientIdentifierValue } from './patient-registration.types';
10
+ import { FormValues, CapturePhotoProps } from './patient-registration.types';
10
11
  import { PatientRegistrationContext } from './patient-registration-context';
11
12
  import { SavePatientForm, SavePatientTransactionManager } from './form-manager';
12
13
  import { usePatientPhoto } from './patient-registration.resource';
13
14
  import { DummyDataInput } from './input/dummy-data/dummy-data-input.component';
14
- import {
15
- cancelRegistration,
16
- filterUndefinedPatientIdenfier,
17
- parseAddressTemplateXml,
18
- scrollIntoView,
19
- } from './patient-registration-utils';
20
- import {
21
- useInitialAddressFieldValues,
22
- useInitialFormValues,
23
- usePatientObs,
24
- usePatientUuidMap,
25
- } from './patient-registration-hooks';
15
+ import { cancelRegistration, filterUndefinedPatientIdenfier, scrollIntoView } from './patient-registration-utils';
16
+ import { useInitialAddressFieldValues, useInitialFormValues, usePatientUuidMap } from './patient-registration-hooks';
26
17
  import { ResourcesContext } from '../offline.resources';
27
18
  import { builtInSections, RegistrationConfig, SectionDefinition } from '../config-schema';
28
19
  import { SectionWrapper } from './section/section-wrapper.component';
@@ -95,17 +86,18 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
95
86
  abortController,
96
87
  );
97
88
 
98
- showToast({
99
- description: inEditMode
100
- ? t('updationSuccessToastDescription', "The patient's information has been successfully updated")
89
+ showSnackbar({
90
+ subtitle: inEditMode
91
+ ? t('updatePatientSuccessSnackbarSubtitle', "The patient's information has been successfully updated")
101
92
  : t(
102
- 'registrationSuccessToastDescription',
93
+ 'registerPatientSuccessSnackbarSubtitle',
103
94
  'The patient can now be found by searching for them using their name or ID number',
104
95
  ),
105
96
  title: inEditMode
106
- ? t('updationSuccessToastTitle', 'Patient Details Updated')
107
- : t('registrationSuccessToastTitle', 'New Patient Created'),
97
+ ? t('updatePatientSuccessSnackbarTitle', 'Patient Details Updated')
98
+ : t('registerPatientSuccessSnackbarTitle', 'New Patient Created'),
108
99
  kind: 'success',
100
+ isLowContrast: true,
109
101
  });
110
102
 
111
103
  const afterUrl = new URLSearchParams(search).get('afterUrl');
@@ -115,10 +107,22 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
115
107
  } catch (error) {
116
108
  if (error.responseBody?.error?.globalErrors) {
117
109
  error.responseBody.error.globalErrors.forEach((error) => {
118
- showToast({ description: error.message });
110
+ showSnackbar({
111
+ title: inEditMode
112
+ ? t('updatePatientErrorSnackbarTitle', 'Patient Details Update Failed')
113
+ : t('registrationErrorSnackbarTitle', 'Patient Registration Failed'),
114
+ subtitle: error.message,
115
+ kind: 'error',
116
+ });
119
117
  });
120
118
  } else if (error.responseBody?.error?.message) {
121
- showToast({ description: error.responseBody.error.message });
119
+ showSnackbar({
120
+ title: inEditMode
121
+ ? t('updatePatientErrorSnackbarTitle', 'Patient Details Update Failed')
122
+ : t('registrationErrorSnackbarTitle', 'Patient Registration Failed'),
123
+ subtitle: error.responseBody.error.message,
124
+ kind: 'error',
125
+ });
122
126
  } else {
123
127
  createErrorHandler()(error);
124
128
  }
@@ -127,25 +131,15 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
127
131
  }
128
132
  };
129
133
 
130
- const getDescription = (errors) => {
131
- return (
132
- <div>
133
- <p>{t('fieldErrorTitleMessage', 'The following fields have errors:')}</p>
134
- <ul style={{ listStyle: 'inside' }}>
135
- {Object.keys(errors).map((error, index) => (
136
- <li key={index}>{t(`${error}LabelText`, error)}</li>
137
- ))}
138
- </ul>
139
- </div>
140
- );
141
- };
142
-
143
134
  const displayErrors = (errors) => {
144
135
  if (errors && typeof errors === 'object' && !!Object.keys(errors).length) {
145
- showToast({
146
- description: getDescription(errors),
147
- title: t('incompleteForm', 'Incomplete form'),
148
- kind: 'warning',
136
+ Object.keys(errors).forEach((error) => {
137
+ showSnackbar({
138
+ subtitle: t(`${error}LabelText`, error),
139
+ title: t('incompleteForm', 'The following field has errors:'),
140
+ kind: 'warning',
141
+ isLowContrast: true,
142
+ });
149
143
  });
150
144
  }
151
145
  };
@@ -168,7 +162,7 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
168
162
  {showDummyData && <DummyDataInput setValues={props.setValues} />}
169
163
  <p className={styles.label01}>{t('jumpTo', 'Jump to')}</p>
170
164
  {sections.map((section) => (
171
- <div className={`${styles.space05} ${styles.touchTarget}`} key={section.name}>
165
+ <div className={classNames(styles.space05, styles.touchTarget)} key={section.name}>
172
166
  <Link className={styles.linkName} onClick={() => scrollIntoView(section.id)}>
173
167
  <XAxis size={16} /> {t(`${section.id}Section`, section.name)}
174
168
  </Link>
@@ -193,8 +187,19 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
193
187
  // Current session and identifiers are required for patient registration.
194
188
  // If currentSession or identifierTypes are not available, then the
195
189
  // user should be blocked to register the patient.
196
- disabled={!enableClientRegistry}>
197
- {inEditMode ? t('updatePatient', 'Update Patient') : t('registerPatient', 'Register Patient')}
190
+ disabled={!currentSession || !identifierTypes || props.isSubmitting || !enableClientRegistry}>
191
+ {props.isSubmitting ? (
192
+ <InlineLoading
193
+ className={styles.spinner}
194
+ description={`${t('submitting', 'Submitting')} ...`}
195
+ iconDescription="submitting"
196
+ status="active"
197
+ />
198
+ ) : inEditMode ? (
199
+ t('updatePatient', 'Update Patient')
200
+ ) : (
201
+ t('registerPatient', 'Register Patient')
202
+ )}
198
203
  </Button>
199
204
  <Button className={styles.cancelButton} kind="tertiary" onClick={cancelRegistration}>
200
205
  {t('cancel', 'Cancel')}
@@ -1,7 +1,5 @@
1
- import { useMemo } from 'react';
2
1
  import useSWR from 'swr';
3
- import useSWRImmutable from 'swr/immutable';
4
- import { FetchResponse, openmrsFetch, useConfig } from '@openmrs/esm-framework';
2
+ import { openmrsFetch, useConfig } from '@openmrs/esm-framework';
5
3
  import { Patient, Relationship, PatientIdentifier, Encounter } from './patient-registration.types';
6
4
 
7
5
  export const uuidIdentifier = '05a29f94-c0ed-11e2-94be-8c13b969e334';
@@ -181,12 +179,25 @@ export function usePatientPhoto(patientUuid: string): UsePatientPhotoResult {
181
179
  };
182
180
  }
183
181
 
184
- export async function fetchPerson(query: string) {
185
- const abortController = new AbortController();
182
+ export async function fetchPerson(query: string, abortController: AbortController) {
183
+ const [patientsRes, personsRes] = await Promise.all([
184
+ openmrsFetch(`/ws/rest/v1/patient?q=${query}`, {
185
+ signal: abortController.signal,
186
+ }),
187
+ openmrsFetch(`/ws/rest/v1/person?q=${query}`, {
188
+ signal: abortController.signal,
189
+ }),
190
+ ]);
186
191
 
187
- return openmrsFetch(`/ws/rest/v1/person?q=${query}`, {
188
- signal: abortController.signal,
192
+ const results = [...patientsRes.data.results];
193
+
194
+ personsRes.data.results.forEach((person) => {
195
+ if (!results.some((patient) => patient.uuid === person.uuid)) {
196
+ results.push(person);
197
+ }
189
198
  });
199
+
200
+ return results;
190
201
  }
191
202
 
192
203
  export async function addPatientIdentifier(patientUuid: string, patientIdentifier: PatientIdentifier) {
@@ -93,6 +93,12 @@
93
93
  }
94
94
  }
95
95
 
96
+ .spinner {
97
+ &:global(.cds--inline-loading) {
98
+ min-height: 1rem;
99
+ }
100
+ }
101
+
96
102
  // Overriding styles for RTL support
97
103
  html[dir='rtl'] {
98
104
  .linkName {