@ampath/esm-login-app 8.0.0-next.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 (190) 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/3378.js +1 -0
  31. package/dist/3748.js +1 -0
  32. package/dist/3748.js.map +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/5098.js +1 -0
  50. package/dist/5098.js.map +1 -0
  51. package/dist/5117.js +1 -0
  52. package/dist/5132.js +1 -0
  53. package/dist/5145.js +1 -0
  54. package/dist/5503.js +1 -0
  55. package/dist/556.js +1 -0
  56. package/dist/5644.js +1 -0
  57. package/dist/5898.js +1 -0
  58. package/dist/5898.js.map +1 -0
  59. package/dist/5940.js +1 -0
  60. package/dist/5976.js +1 -0
  61. package/dist/5976.js.map +1 -0
  62. package/dist/6047.js +1 -0
  63. package/dist/6237.js +1 -0
  64. package/dist/6237.js.map +1 -0
  65. package/dist/6362.js +1 -0
  66. package/dist/6362.js.map +1 -0
  67. package/dist/6371.js +1 -0
  68. package/dist/6377.js +1 -0
  69. package/dist/6444.js +1 -0
  70. package/dist/6508.js +1 -0
  71. package/dist/6724.js +1 -0
  72. package/dist/6904.js +1 -0
  73. package/dist/7045.js +1 -0
  74. package/dist/7144.js +43 -0
  75. package/dist/7144.js.map +1 -0
  76. package/dist/7175.js +1 -0
  77. package/dist/7182.js +1 -0
  78. package/dist/7251.js +1 -0
  79. package/dist/7251.js.map +1 -0
  80. package/dist/749.js +1 -0
  81. package/dist/749.js.map +1 -0
  82. package/dist/7742.js +1 -0
  83. package/dist/7912.js +1 -0
  84. package/dist/8358.js +1 -0
  85. package/dist/8359.js +1 -0
  86. package/dist/8695.js +1 -0
  87. package/dist/903.js +1 -0
  88. package/dist/9072.js +1 -0
  89. package/dist/9510.js +15 -0
  90. package/dist/9510.js.map +1 -0
  91. package/dist/9806.js +1 -0
  92. package/dist/esm-login-app.js +6 -0
  93. package/dist/esm-login-app.js.buildmanifest.json +1584 -0
  94. package/dist/esm-login-app.js.map +1 -0
  95. package/dist/main.js +6 -0
  96. package/dist/main.js.map +1 -0
  97. package/dist/routes.json +1 -0
  98. package/jest.config.js +20 -0
  99. package/package.json +111 -0
  100. package/prettier.config.js +8 -0
  101. package/rspack.config.js +1 -0
  102. package/src/change-location-link/change-location-link.extension.tsx +32 -0
  103. package/src/change-location-link/change-location-link.scss +17 -0
  104. package/src/change-location-link/change-location-link.test.tsx +36 -0
  105. package/src/change-password/change-password-link.extension.tsx +30 -0
  106. package/src/change-password/change-password-link.test.tsx +27 -0
  107. package/src/change-password/change-password-modal.scss +11 -0
  108. package/src/change-password/change-password.component.tsx +159 -0
  109. package/src/change-password/change-password.modal.tsx +175 -0
  110. package/src/change-password/change-password.resource.ts +12 -0
  111. package/src/change-password/change-password.scss +51 -0
  112. package/src/change-password/change-password.test.tsx +53 -0
  113. package/src/common/otp/otp.component.tsx +54 -0
  114. package/src/common/otp/otp.scss +13 -0
  115. package/src/common/resend-timer/resend-timer.component.tsx +51 -0
  116. package/src/common/resend-timer/resend-timer.scss +7 -0
  117. package/src/config-schema.ts +145 -0
  118. package/src/declarations.d.ts +1 -0
  119. package/src/footer.component.tsx +60 -0
  120. package/src/footer.scss +113 -0
  121. package/src/index.ts +27 -0
  122. package/src/loading/loading.component.tsx +11 -0
  123. package/src/loading/loading.scss +7 -0
  124. package/src/location-picker/location-picker-view.component.tsx +174 -0
  125. package/src/location-picker/location-picker.resource.ts +111 -0
  126. package/src/location-picker/location-picker.scss +94 -0
  127. package/src/location-picker/location-picker.test.tsx +341 -0
  128. package/src/login/login.component.tsx +329 -0
  129. package/src/login/login.scss +167 -0
  130. package/src/login/login.test.tsx +288 -0
  131. package/src/login.resource.ts +147 -0
  132. package/src/logo.component.tsx +23 -0
  133. package/src/logout/logout.extension.tsx +23 -0
  134. package/src/logout/logout.scss +12 -0
  135. package/src/otp/otp.component.tsx +105 -0
  136. package/src/otp/otp.scss +46 -0
  137. package/src/redirect-logout/logout.resource.ts +15 -0
  138. package/src/redirect-logout/redirect-logout.component.tsx +42 -0
  139. package/src/redirect-logout/redirect-logout.test.tsx +180 -0
  140. package/src/resources/otp.resource.ts +51 -0
  141. package/src/root.component.tsx +24 -0
  142. package/src/routes.json +63 -0
  143. package/src/setupTests.ts +15 -0
  144. package/src/test-helpers/render-with-router.tsx +17 -0
  145. package/src/types.ts +34 -0
  146. package/src/utils/get-base-url.ts +7 -0
  147. package/translations/am.json +41 -0
  148. package/translations/ar.json +41 -0
  149. package/translations/ar_SY.json +41 -0
  150. package/translations/bn.json +41 -0
  151. package/translations/cs.json +41 -0
  152. package/translations/de.json +41 -0
  153. package/translations/en.json +41 -0
  154. package/translations/en_US.json +41 -0
  155. package/translations/es.json +41 -0
  156. package/translations/es_MX.json +41 -0
  157. package/translations/fr.json +41 -0
  158. package/translations/he.json +41 -0
  159. package/translations/hi.json +41 -0
  160. package/translations/hi_IN.json +41 -0
  161. package/translations/id.json +41 -0
  162. package/translations/it.json +41 -0
  163. package/translations/ka.json +41 -0
  164. package/translations/km.json +41 -0
  165. package/translations/ku.json +41 -0
  166. package/translations/ky.json +41 -0
  167. package/translations/lg.json +41 -0
  168. package/translations/ne.json +41 -0
  169. package/translations/pl.json +41 -0
  170. package/translations/pt.json +41 -0
  171. package/translations/pt_BR.json +41 -0
  172. package/translations/qu.json +41 -0
  173. package/translations/ro_RO.json +41 -0
  174. package/translations/ru_RU.json +41 -0
  175. package/translations/si.json +41 -0
  176. package/translations/sq.json +41 -0
  177. package/translations/sw.json +41 -0
  178. package/translations/sw_KE.json +41 -0
  179. package/translations/tr.json +41 -0
  180. package/translations/tr_TR.json +41 -0
  181. package/translations/uk.json +41 -0
  182. package/translations/uz.json +41 -0
  183. package/translations/uz@Latn.json +41 -0
  184. package/translations/uz_UZ.json +41 -0
  185. package/translations/vi.json +41 -0
  186. package/translations/zh.json +41 -0
  187. package/translations/zh_CN.json +41 -0
  188. package/translations/zh_TW.json +41 -0
  189. package/tsconfig.json +25 -0
  190. 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
+ }