@ampath/esm-login-app 8.0.0-next.10

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 (202) hide show
  1. package/.editorconfig +8 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc +76 -0
  4. package/.gitattributes +4 -0
  5. package/.prettierignore +9 -0
  6. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  7. package/.yarn/versions/643d2b70.yml +21 -0
  8. package/README.md +4 -0
  9. package/__mocks__/config.mock.ts +26 -0
  10. package/__mocks__/locations.mock.ts +540 -0
  11. package/__mocks__/react-i18next.js +49 -0
  12. package/dist/1128.js +1 -0
  13. package/dist/1128.js.map +1 -0
  14. package/dist/1480.js +1 -0
  15. package/dist/1578.js +1 -0
  16. package/dist/1578.js.map +1 -0
  17. package/dist/1646.js +1 -0
  18. package/dist/1800.js +1 -0
  19. package/dist/1800.js.map +1 -0
  20. package/dist/1869.js +1 -0
  21. package/dist/1877.js +1 -0
  22. package/dist/2317.js +1 -0
  23. package/dist/2416.js +1 -0
  24. package/dist/2489.js +1 -0
  25. package/dist/2489.js.map +1 -0
  26. package/dist/282.js +1 -0
  27. package/dist/2881.js +1 -0
  28. package/dist/2997.js +1 -0
  29. package/dist/2997.js.map +1 -0
  30. package/dist/3219.js +1 -0
  31. package/dist/3219.js.map +1 -0
  32. package/dist/3378.js +1 -0
  33. package/dist/3963.js +1 -0
  34. package/dist/4106.js +1 -0
  35. package/dist/4111.js +1 -0
  36. package/dist/4169.js +1 -0
  37. package/dist/4169.js.map +1 -0
  38. package/dist/434.js +1 -0
  39. package/dist/4348.js +1 -0
  40. package/dist/4378.js +1 -0
  41. package/dist/4378.js.map +1 -0
  42. package/dist/4383.js +1 -0
  43. package/dist/4658.js +1 -0
  44. package/dist/4668.js +1 -0
  45. package/dist/4668.js.map +1 -0
  46. package/dist/4870.js +1 -0
  47. package/dist/4870.js.map +1 -0
  48. package/dist/4928.js +1 -0
  49. package/dist/4bc56e5b0b0e91da.png +0 -0
  50. package/dist/5098.js +1 -0
  51. package/dist/5098.js.map +1 -0
  52. package/dist/5117.js +1 -0
  53. package/dist/5132.js +1 -0
  54. package/dist/5145.js +1 -0
  55. package/dist/5503.js +1 -0
  56. package/dist/556.js +1 -0
  57. package/dist/5644.js +1 -0
  58. package/dist/5898.js +1 -0
  59. package/dist/5898.js.map +1 -0
  60. package/dist/5940.js +1 -0
  61. package/dist/5976.js +1 -0
  62. package/dist/5976.js.map +1 -0
  63. package/dist/6047.js +1 -0
  64. package/dist/6237.js +1 -0
  65. package/dist/6237.js.map +1 -0
  66. package/dist/6362.js +1 -0
  67. package/dist/6362.js.map +1 -0
  68. package/dist/6371.js +1 -0
  69. package/dist/6377.js +1 -0
  70. package/dist/6444.js +1 -0
  71. package/dist/647e55b5cedf5df2.png +0 -0
  72. package/dist/6508.js +1 -0
  73. package/dist/6724.js +1 -0
  74. package/dist/6904.js +1 -0
  75. package/dist/7045.js +1 -0
  76. package/dist/7144.js +43 -0
  77. package/dist/7144.js.map +1 -0
  78. package/dist/7175.js +1 -0
  79. package/dist/7182.js +1 -0
  80. package/dist/7251.js +1 -0
  81. package/dist/7251.js.map +1 -0
  82. package/dist/749.js +1 -0
  83. package/dist/749.js.map +1 -0
  84. package/dist/7742.js +1 -0
  85. package/dist/7912.js +1 -0
  86. package/dist/8358.js +1 -0
  87. package/dist/8359.js +1 -0
  88. package/dist/8695.js +1 -0
  89. package/dist/903.js +1 -0
  90. package/dist/9072.js +1 -0
  91. package/dist/9510.js +15 -0
  92. package/dist/9510.js.map +1 -0
  93. package/dist/9806.js +1 -0
  94. package/dist/a6792134b9df70c4.png +0 -0
  95. package/dist/acd6ab71c5f6bcb6.jpg +0 -0
  96. package/dist/d0bf081185f017f3.jpg +0 -0
  97. package/dist/d48e253df6a333a7.png +0 -0
  98. package/dist/esm-login-app.js +6 -0
  99. package/dist/esm-login-app.js.buildmanifest.json +1598 -0
  100. package/dist/esm-login-app.js.map +1 -0
  101. package/dist/main.js +6 -0
  102. package/dist/main.js.map +1 -0
  103. package/dist/routes.json +1 -0
  104. package/jest.config.js +20 -0
  105. package/package.json +111 -0
  106. package/prettier.config.js +8 -0
  107. package/rspack.config.js +1 -0
  108. package/src/assets/Taifa-Care.png +0 -0
  109. package/src/assets/ampath-logo.png +0 -0
  110. package/src/assets/dha.png +0 -0
  111. package/src/assets/gok.png +0 -0
  112. package/src/assets/medicine.jpg +0 -0
  113. package/src/assets/openmrs.jpg +0 -0
  114. package/src/change-location-link/change-location-link.extension.tsx +32 -0
  115. package/src/change-location-link/change-location-link.scss +17 -0
  116. package/src/change-location-link/change-location-link.test.tsx +36 -0
  117. package/src/change-password/change-password-link.extension.tsx +30 -0
  118. package/src/change-password/change-password-link.test.tsx +27 -0
  119. package/src/change-password/change-password-modal.scss +11 -0
  120. package/src/change-password/change-password.component.tsx +159 -0
  121. package/src/change-password/change-password.modal.tsx +175 -0
  122. package/src/change-password/change-password.resource.ts +12 -0
  123. package/src/change-password/change-password.scss +51 -0
  124. package/src/change-password/change-password.test.tsx +53 -0
  125. package/src/common/otp/otp.component.tsx +54 -0
  126. package/src/common/otp/otp.scss +13 -0
  127. package/src/common/resend-timer/resend-timer.component.tsx +56 -0
  128. package/src/common/resend-timer/resend-timer.scss +7 -0
  129. package/src/config-schema.ts +140 -0
  130. package/src/declarations.d.ts +5 -0
  131. package/src/footer.component.tsx +60 -0
  132. package/src/footer.scss +113 -0
  133. package/src/index.ts +27 -0
  134. package/src/loading/loading.component.tsx +11 -0
  135. package/src/loading/loading.scss +7 -0
  136. package/src/location-picker/location-picker-view.component.tsx +174 -0
  137. package/src/location-picker/location-picker.resource.ts +111 -0
  138. package/src/location-picker/location-picker.scss +94 -0
  139. package/src/location-picker/location-picker.test.tsx +341 -0
  140. package/src/login/login.component.tsx +350 -0
  141. package/src/login/login.scss +246 -0
  142. package/src/login/login.test.tsx +288 -0
  143. package/src/login.resource.ts +147 -0
  144. package/src/logo.component.tsx +22 -0
  145. package/src/logout/logout.extension.tsx +23 -0
  146. package/src/logout/logout.scss +12 -0
  147. package/src/otp/otp.component.tsx +108 -0
  148. package/src/otp/otp.module.scss +44 -0
  149. package/src/redirect-logout/logout.resource.ts +15 -0
  150. package/src/redirect-logout/redirect-logout.component.tsx +42 -0
  151. package/src/redirect-logout/redirect-logout.test.tsx +180 -0
  152. package/src/resources/otp.resource.ts +90 -0
  153. package/src/root.component.tsx +24 -0
  154. package/src/routes.json +63 -0
  155. package/src/setupTests.ts +15 -0
  156. package/src/test-helpers/render-with-router.tsx +17 -0
  157. package/src/types.ts +34 -0
  158. package/src/utils/get-base-url.ts +17 -0
  159. package/translations/am.json +41 -0
  160. package/translations/ar.json +41 -0
  161. package/translations/ar_SY.json +41 -0
  162. package/translations/bn.json +41 -0
  163. package/translations/cs.json +41 -0
  164. package/translations/de.json +41 -0
  165. package/translations/en.json +41 -0
  166. package/translations/en_US.json +41 -0
  167. package/translations/es.json +41 -0
  168. package/translations/es_MX.json +41 -0
  169. package/translations/fr.json +41 -0
  170. package/translations/he.json +41 -0
  171. package/translations/hi.json +41 -0
  172. package/translations/hi_IN.json +41 -0
  173. package/translations/id.json +41 -0
  174. package/translations/it.json +41 -0
  175. package/translations/ka.json +41 -0
  176. package/translations/km.json +41 -0
  177. package/translations/ku.json +41 -0
  178. package/translations/ky.json +41 -0
  179. package/translations/lg.json +41 -0
  180. package/translations/ne.json +41 -0
  181. package/translations/pl.json +41 -0
  182. package/translations/pt.json +41 -0
  183. package/translations/pt_BR.json +41 -0
  184. package/translations/qu.json +41 -0
  185. package/translations/ro_RO.json +41 -0
  186. package/translations/ru_RU.json +41 -0
  187. package/translations/si.json +41 -0
  188. package/translations/sq.json +41 -0
  189. package/translations/sw.json +41 -0
  190. package/translations/sw_KE.json +41 -0
  191. package/translations/tr.json +41 -0
  192. package/translations/tr_TR.json +41 -0
  193. package/translations/uk.json +41 -0
  194. package/translations/uz.json +41 -0
  195. package/translations/uz@Latn.json +41 -0
  196. package/translations/uz_UZ.json +41 -0
  197. package/translations/vi.json +41 -0
  198. package/translations/zh.json +41 -0
  199. package/translations/zh_CN.json +41 -0
  200. package/translations/zh_TW.json +41 -0
  201. package/tsconfig.json +25 -0
  202. package/yarnrc.yml +14 -0
@@ -0,0 +1,174 @@
1
+ import React, { useCallback, useEffect, useId, useMemo, useState } from 'react';
2
+ import { Button, Checkbox, InlineLoading } from '@carbon/react';
3
+ import { useLocation, type Location, useSearchParams } from 'react-router-dom';
4
+ import { useTranslation } from 'react-i18next';
5
+ import {
6
+ getCoreTranslation,
7
+ LocationPicker,
8
+ navigate,
9
+ setSessionLocation,
10
+ useConfig,
11
+ useConnectivity,
12
+ useSession,
13
+ } from '@openmrs/esm-framework';
14
+ import { useDefaultLocation, useLocationCount } from './location-picker.resource';
15
+ import type { ConfigSchema } from '../config-schema';
16
+ import type { LoginReferrer } from '../login/login.component';
17
+ import styles from './location-picker.scss';
18
+
19
+ interface LocationPickerProps {
20
+ hideWelcomeMessage?: boolean;
21
+ currentLocationUuid?: string;
22
+ }
23
+
24
+ const LocationPickerView: React.FC<LocationPickerProps> = ({ hideWelcomeMessage, currentLocationUuid }) => {
25
+ const { t } = useTranslation();
26
+ const config = useConfig<ConfigSchema>();
27
+ const { chooseLocation } = config;
28
+ const isLoginEnabled = useConnectivity();
29
+ const [searchParams] = useSearchParams();
30
+ const checkboxId = useId();
31
+ const isUpdateFlow = useMemo(() => searchParams.get('update') === 'true', [searchParams]);
32
+ const { defaultLocation, updateDefaultLocation, savePreference, setSavePreference } =
33
+ useDefaultLocation(isUpdateFlow);
34
+ const {
35
+ isLoading: isLoadingLocationCount,
36
+ locationCount,
37
+ firstLocation,
38
+ } = useLocationCount(chooseLocation.useLoginLocationTag);
39
+
40
+ const { user, sessionLocation } = useSession();
41
+ const { currentUser, userProperties } = useMemo(
42
+ () => ({
43
+ currentUser: user?.display,
44
+ userProperties: user?.userProperties,
45
+ }),
46
+ [user],
47
+ );
48
+
49
+ const [activeLocation, setActiveLocation] = useState(() => {
50
+ if (currentLocationUuid && hideWelcomeMessage) {
51
+ return currentLocationUuid;
52
+ }
53
+ return sessionLocation?.uuid ?? defaultLocation;
54
+ });
55
+
56
+ const [isSubmitting, setIsSubmitting] = useState(false);
57
+
58
+ const { state } = useLocation() as unknown as Omit<Location, 'state'> & {
59
+ state: LoginReferrer;
60
+ };
61
+
62
+ const changeLocation = useCallback(
63
+ (locationUuid?: string, saveUserPreference?: boolean) => {
64
+ setIsSubmitting(true);
65
+
66
+ const referrer = state?.referrer;
67
+ const returnToUrl = searchParams.get('returnToUrl');
68
+
69
+ const sessionDefined = setSessionLocation(locationUuid, new AbortController());
70
+
71
+ updateDefaultLocation(locationUuid, saveUserPreference);
72
+ sessionDefined.then(() => {
73
+ if (referrer && !['/', '/login', '/login/location'].includes(referrer)) {
74
+ navigate({ to: '${openmrsSpaBase}' + referrer });
75
+ return;
76
+ }
77
+ if (returnToUrl && returnToUrl !== '/') {
78
+ navigate({ to: returnToUrl });
79
+ } else {
80
+ navigate({ to: config.links.loginSuccess });
81
+ }
82
+ });
83
+ },
84
+ [state?.referrer, config.links.loginSuccess, updateDefaultLocation, searchParams],
85
+ );
86
+
87
+ // Handle cases where the location picker is disabled, there is only one location, or there are no locations.
88
+ useEffect(() => {
89
+ if (isLoadingLocationCount) return;
90
+
91
+ if (locationCount === 0) {
92
+ changeLocation();
93
+ } else if (locationCount === 1 || !chooseLocation.enabled) {
94
+ if (firstLocation?.resource?.id) {
95
+ changeLocation(firstLocation.resource.id, true);
96
+ } else {
97
+ console.error('Expected location data is missing', { firstLocation, locationCount });
98
+ }
99
+ }
100
+ }, [locationCount, isLoadingLocationCount]);
101
+
102
+ // Handle cases where the login location is present in the userProperties.
103
+ useEffect(() => {
104
+ if (isUpdateFlow) {
105
+ return;
106
+ }
107
+ if (defaultLocation && !isSubmitting) {
108
+ setActiveLocation(defaultLocation);
109
+ changeLocation(defaultLocation, true);
110
+ }
111
+ }, [changeLocation, isSubmitting, defaultLocation, isUpdateFlow]);
112
+
113
+ const handleSubmit = useCallback(
114
+ (evt: React.FormEvent<HTMLFormElement>) => {
115
+ evt.preventDefault();
116
+
117
+ if (!activeLocation) {
118
+ return;
119
+ }
120
+
121
+ changeLocation(activeLocation, savePreference);
122
+ },
123
+ [activeLocation, changeLocation, savePreference],
124
+ );
125
+
126
+ return (
127
+ <div className={styles.locationPickerContainer}>
128
+ <form onSubmit={handleSubmit}>
129
+ <div className={styles.locationCard}>
130
+ <div className={styles.paddedContainer}>
131
+ <p className={styles.welcomeTitle}>
132
+ {t('welcome', 'Welcome')} {currentUser}
133
+ </p>
134
+ <p className={styles.welcomeMessage}>
135
+ {t(
136
+ 'selectYourLocation',
137
+ 'Select your location from the list below. Use the search bar to find your location.',
138
+ )}
139
+ </p>
140
+ </div>
141
+ <LocationPicker
142
+ selectedLocationUuid={activeLocation}
143
+ defaultLocationUuid={userProperties.defaultLocation}
144
+ locationTag={chooseLocation.useLoginLocationTag && 'Login Location'}
145
+ onChange={(locationUuid) => setActiveLocation(locationUuid)}
146
+ />
147
+ <div className={styles.footerContainer}>
148
+ <Checkbox
149
+ className={styles.savePreferenceCheckbox}
150
+ checked={savePreference}
151
+ id={checkboxId}
152
+ labelText={t('rememberLocationForFutureLogins', 'Remember my location for future logins')}
153
+ onChange={(_, { checked }) => setSavePreference(checked)}
154
+ />
155
+ <Button
156
+ className={styles.confirmButton}
157
+ kind="primary"
158
+ type="submit"
159
+ disabled={!activeLocation || !isLoginEnabled || isSubmitting}
160
+ >
161
+ {isSubmitting ? (
162
+ <InlineLoading className={styles.loader} description={t('submitting', 'Submitting')} />
163
+ ) : (
164
+ <span>{getCoreTranslation('confirm')}</span>
165
+ )}
166
+ </Button>
167
+ </div>
168
+ </div>
169
+ </form>
170
+ </div>
171
+ );
172
+ };
173
+
174
+ export default LocationPickerView;
@@ -0,0 +1,111 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import useSwrImmutable from 'swr/immutable';
4
+ import { type FetchResponse, openmrsFetch, setUserProperties, showSnackbar, useSession } from '@openmrs/esm-framework';
5
+ import { useValidateLocationUuid } from '../login.resource';
6
+ import { type LocationResponse } from '../types';
7
+
8
+ export function useDefaultLocation(isUpdateFlow: boolean) {
9
+ const { t } = useTranslation();
10
+ const { user } = useSession();
11
+ const { userUuid, userProperties } = useMemo(
12
+ () => ({
13
+ userUuid: user?.uuid,
14
+ userProperties: user?.userProperties,
15
+ }),
16
+ [user],
17
+ );
18
+ const [savePreference, setSavePreference] = useState(false);
19
+
20
+ const defaultLocation = useMemo(() => userProperties?.defaultLocation, [userProperties?.defaultLocation]);
21
+
22
+ const { isLocationValid, defaultLocation: defaultLocationFhir } = useValidateLocationUuid(defaultLocation);
23
+
24
+ useEffect(() => {
25
+ if (defaultLocation) {
26
+ setSavePreference(true);
27
+ }
28
+ }, [setSavePreference, defaultLocation]);
29
+
30
+ const updateUserPropsWithDefaultLocation = useCallback(
31
+ async (locationUuid: string, saveDefaultLocation: boolean) => {
32
+ if (saveDefaultLocation) {
33
+ // If the user checks the checkbox for saving the preference
34
+ const updatedUserProperties = {
35
+ ...userProperties,
36
+ defaultLocation: locationUuid,
37
+ };
38
+ await setUserProperties(userUuid, updatedUserProperties);
39
+ } else if (!!userProperties?.defaultLocation) {
40
+ // If the user doesn't want to save the preference,
41
+ // the old preference should be deleted
42
+ const updatedUserProperties = { ...userProperties };
43
+ delete updatedUserProperties.defaultLocation;
44
+ await setUserProperties(userUuid, updatedUserProperties);
45
+ }
46
+ },
47
+ [userProperties, userUuid],
48
+ );
49
+
50
+ const updateDefaultLocation = useCallback(
51
+ async (locationUuid: string, saveDefaultLocation: boolean) => {
52
+ if (savePreference && locationUuid === defaultLocation) {
53
+ return;
54
+ }
55
+
56
+ updateUserPropsWithDefaultLocation(locationUuid, saveDefaultLocation).then(() => {
57
+ if (saveDefaultLocation) {
58
+ showSnackbar({
59
+ title: !isUpdateFlow ? t('locationSaved', 'Location saved') : t('locationUpdated', 'Location updated'),
60
+ subtitle: !isUpdateFlow
61
+ ? t('locationSaveMessage', 'Your preferred location has been saved for future logins')
62
+ : t('locationUpdateMessage', 'Your preferred login location has been updated'),
63
+ kind: 'success',
64
+ isLowContrast: true,
65
+ });
66
+ } else if (defaultLocation) {
67
+ showSnackbar({
68
+ title: t('locationPreferenceRemoved', 'Location preference removed'),
69
+ subtitle: t('locationPreferenceRemovedMessage', 'You will need to select a location on each login'),
70
+ kind: 'success',
71
+ isLowContrast: true,
72
+ });
73
+ }
74
+ });
75
+ },
76
+ [savePreference, defaultLocation, updateUserPropsWithDefaultLocation, t, isUpdateFlow],
77
+ );
78
+
79
+ return {
80
+ defaultLocationFhir,
81
+ defaultLocation: isLocationValid ? defaultLocation : null,
82
+ updateDefaultLocation,
83
+ savePreference,
84
+ setSavePreference,
85
+ };
86
+ }
87
+
88
+ export function useLocationCount(useLoginLocationTag: boolean) {
89
+ const url = `/ws/fhir2/R4/Location?_count=1`;
90
+ if (useLoginLocationTag) {
91
+ url.concat(`&tag=Login Location`);
92
+ }
93
+ const { data, error, isLoading } = useSwrImmutable<FetchResponse<LocationResponse>>(url, openmrsFetch, {
94
+ shouldRetryOnError(err) {
95
+ if (err?.response?.status) {
96
+ return err.response.status >= 500;
97
+ }
98
+ return false;
99
+ },
100
+ });
101
+
102
+ return useMemo(
103
+ () => ({
104
+ locationCount: data?.data?.total,
105
+ firstLocation: data?.data?.entry ? data.data.entry[0] : null,
106
+ error,
107
+ isLoading,
108
+ }),
109
+ [data, isLoading, error],
110
+ );
111
+ }
@@ -0,0 +1,94 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .bodyLong01 {
6
+ @include type.type-style('body-01');
7
+ }
8
+
9
+ .productiveHeading03 {
10
+ @include type.type-style('heading-03');
11
+ }
12
+
13
+ .backButton {
14
+ height: layout.$spacing-09;
15
+ padding-left: 0;
16
+
17
+ svg {
18
+ margin-right: layout.$spacing-03;
19
+ order: 1;
20
+ }
21
+
22
+ span {
23
+ order: 2;
24
+ }
25
+ }
26
+
27
+ .locationPickerContainer {
28
+ display: flex;
29
+ flex-direction: column;
30
+ justify-content: center;
31
+ align-items: center;
32
+ height: 100vh;
33
+ width: 100%;
34
+ }
35
+
36
+ .paddedContainer {
37
+ padding: layout.$spacing-06 layout.$spacing-06 layout.$spacing-05;
38
+ }
39
+
40
+ .locationCard {
41
+ display: flex;
42
+ flex-direction: column;
43
+ width: 23rem;
44
+ background-color: $ui-02;
45
+ margin: 0;
46
+ border: 1px solid $ui-03;
47
+ }
48
+
49
+ .welcomeTitle {
50
+ @extend .productiveHeading03;
51
+ text-transform: capitalize;
52
+ }
53
+
54
+ .welcomeMessage {
55
+ @extend .bodyLong01;
56
+ margin-top: layout.$spacing-03;
57
+ color: $color-gray-70;
58
+ }
59
+
60
+ .footerContainer {
61
+ padding: layout.$spacing-05 layout.$spacing-06 layout.$spacing-06;
62
+
63
+ .savePreferenceCheckbox {
64
+ padding-bottom: layout.$spacing-05 !important;
65
+ }
66
+
67
+ .confirmButton {
68
+ width: 100%;
69
+ }
70
+ }
71
+
72
+ .cancelButton {
73
+ margin-top: layout.$spacing-02;
74
+ }
75
+
76
+ .loadingContainer {
77
+ margin-top: layout.$spacing-06;
78
+ }
79
+
80
+ .emptyState {
81
+ display: flex;
82
+ flex-direction: column;
83
+ justify-content: center;
84
+ margin: layout.$spacing-06 auto 0;
85
+ }
86
+
87
+ .loader {
88
+ min-height: fit-content;
89
+ }
90
+
91
+ .errorNotification {
92
+ margin-inline-start: -(layout.$spacing-06);
93
+ margin-inline-end: -(layout.$spacing-06);
94
+ }