@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.
- package/.editorconfig +8 -0
- package/.eslintignore +3 -0
- package/.eslintrc +76 -0
- package/.gitattributes +4 -0
- package/.prettierignore +9 -0
- package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
- package/.yarn/versions/643d2b70.yml +21 -0
- package/README.md +4 -0
- package/__mocks__/config.mock.ts +26 -0
- package/__mocks__/locations.mock.ts +540 -0
- package/__mocks__/react-i18next.js +49 -0
- package/dist/1128.js +1 -0
- package/dist/1128.js.map +1 -0
- package/dist/1480.js +1 -0
- package/dist/1578.js +1 -0
- package/dist/1578.js.map +1 -0
- package/dist/1646.js +1 -0
- package/dist/1800.js +1 -0
- package/dist/1800.js.map +1 -0
- package/dist/1869.js +1 -0
- package/dist/1877.js +1 -0
- package/dist/2317.js +1 -0
- package/dist/2416.js +1 -0
- package/dist/2489.js +1 -0
- package/dist/2489.js.map +1 -0
- package/dist/282.js +1 -0
- package/dist/2881.js +1 -0
- package/dist/2997.js +1 -0
- package/dist/2997.js.map +1 -0
- package/dist/3219.js +1 -0
- package/dist/3219.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3963.js +1 -0
- package/dist/4106.js +1 -0
- package/dist/4111.js +1 -0
- package/dist/4169.js +1 -0
- package/dist/4169.js.map +1 -0
- package/dist/434.js +1 -0
- package/dist/4348.js +1 -0
- package/dist/4378.js +1 -0
- package/dist/4378.js.map +1 -0
- package/dist/4383.js +1 -0
- package/dist/4658.js +1 -0
- package/dist/4668.js +1 -0
- package/dist/4668.js.map +1 -0
- package/dist/4870.js +1 -0
- package/dist/4870.js.map +1 -0
- package/dist/4928.js +1 -0
- package/dist/4bc56e5b0b0e91da.png +0 -0
- package/dist/5098.js +1 -0
- package/dist/5098.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5132.js +1 -0
- package/dist/5145.js +1 -0
- package/dist/5503.js +1 -0
- package/dist/556.js +1 -0
- package/dist/5644.js +1 -0
- package/dist/5898.js +1 -0
- package/dist/5898.js.map +1 -0
- package/dist/5940.js +1 -0
- package/dist/5976.js +1 -0
- package/dist/5976.js.map +1 -0
- package/dist/6047.js +1 -0
- package/dist/6237.js +1 -0
- package/dist/6237.js.map +1 -0
- package/dist/6362.js +1 -0
- package/dist/6362.js.map +1 -0
- package/dist/6371.js +1 -0
- package/dist/6377.js +1 -0
- package/dist/6444.js +1 -0
- package/dist/647e55b5cedf5df2.png +0 -0
- package/dist/6508.js +1 -0
- package/dist/6724.js +1 -0
- package/dist/6904.js +1 -0
- package/dist/7045.js +1 -0
- package/dist/7144.js +43 -0
- package/dist/7144.js.map +1 -0
- package/dist/7175.js +1 -0
- package/dist/7182.js +1 -0
- package/dist/7251.js +1 -0
- package/dist/7251.js.map +1 -0
- package/dist/749.js +1 -0
- package/dist/749.js.map +1 -0
- package/dist/7742.js +1 -0
- package/dist/7912.js +1 -0
- package/dist/8358.js +1 -0
- package/dist/8359.js +1 -0
- package/dist/8695.js +1 -0
- package/dist/903.js +1 -0
- package/dist/9072.js +1 -0
- package/dist/9510.js +15 -0
- package/dist/9510.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/a6792134b9df70c4.png +0 -0
- package/dist/acd6ab71c5f6bcb6.jpg +0 -0
- package/dist/d0bf081185f017f3.jpg +0 -0
- package/dist/d48e253df6a333a7.png +0 -0
- package/dist/esm-login-app.js +6 -0
- package/dist/esm-login-app.js.buildmanifest.json +1598 -0
- package/dist/esm-login-app.js.map +1 -0
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +20 -0
- package/package.json +111 -0
- package/prettier.config.js +8 -0
- package/rspack.config.js +1 -0
- package/src/assets/Taifa-Care.png +0 -0
- package/src/assets/ampath-logo.png +0 -0
- package/src/assets/dha.png +0 -0
- package/src/assets/gok.png +0 -0
- package/src/assets/medicine.jpg +0 -0
- package/src/assets/openmrs.jpg +0 -0
- package/src/change-location-link/change-location-link.extension.tsx +32 -0
- package/src/change-location-link/change-location-link.scss +17 -0
- package/src/change-location-link/change-location-link.test.tsx +36 -0
- package/src/change-password/change-password-link.extension.tsx +30 -0
- package/src/change-password/change-password-link.test.tsx +27 -0
- package/src/change-password/change-password-modal.scss +11 -0
- package/src/change-password/change-password.component.tsx +159 -0
- package/src/change-password/change-password.modal.tsx +175 -0
- package/src/change-password/change-password.resource.ts +12 -0
- package/src/change-password/change-password.scss +51 -0
- package/src/change-password/change-password.test.tsx +53 -0
- package/src/common/otp/otp.component.tsx +54 -0
- package/src/common/otp/otp.scss +13 -0
- package/src/common/resend-timer/resend-timer.component.tsx +56 -0
- package/src/common/resend-timer/resend-timer.scss +7 -0
- package/src/config-schema.ts +140 -0
- package/src/declarations.d.ts +5 -0
- package/src/footer.component.tsx +60 -0
- package/src/footer.scss +113 -0
- package/src/index.ts +27 -0
- package/src/loading/loading.component.tsx +11 -0
- package/src/loading/loading.scss +7 -0
- package/src/location-picker/location-picker-view.component.tsx +174 -0
- package/src/location-picker/location-picker.resource.ts +111 -0
- package/src/location-picker/location-picker.scss +94 -0
- package/src/location-picker/location-picker.test.tsx +341 -0
- package/src/login/login.component.tsx +350 -0
- package/src/login/login.scss +246 -0
- package/src/login/login.test.tsx +288 -0
- package/src/login.resource.ts +147 -0
- package/src/logo.component.tsx +22 -0
- package/src/logout/logout.extension.tsx +23 -0
- package/src/logout/logout.scss +12 -0
- package/src/otp/otp.component.tsx +108 -0
- package/src/otp/otp.module.scss +44 -0
- package/src/redirect-logout/logout.resource.ts +15 -0
- package/src/redirect-logout/redirect-logout.component.tsx +42 -0
- package/src/redirect-logout/redirect-logout.test.tsx +180 -0
- package/src/resources/otp.resource.ts +90 -0
- package/src/root.component.tsx +24 -0
- package/src/routes.json +63 -0
- package/src/setupTests.ts +15 -0
- package/src/test-helpers/render-with-router.tsx +17 -0
- package/src/types.ts +34 -0
- package/src/utils/get-base-url.ts +17 -0
- package/translations/am.json +41 -0
- package/translations/ar.json +41 -0
- package/translations/ar_SY.json +41 -0
- package/translations/bn.json +41 -0
- package/translations/cs.json +41 -0
- package/translations/de.json +41 -0
- package/translations/en.json +41 -0
- package/translations/en_US.json +41 -0
- package/translations/es.json +41 -0
- package/translations/es_MX.json +41 -0
- package/translations/fr.json +41 -0
- package/translations/he.json +41 -0
- package/translations/hi.json +41 -0
- package/translations/hi_IN.json +41 -0
- package/translations/id.json +41 -0
- package/translations/it.json +41 -0
- package/translations/ka.json +41 -0
- package/translations/km.json +41 -0
- package/translations/ku.json +41 -0
- package/translations/ky.json +41 -0
- package/translations/lg.json +41 -0
- package/translations/ne.json +41 -0
- package/translations/pl.json +41 -0
- package/translations/pt.json +41 -0
- package/translations/pt_BR.json +41 -0
- package/translations/qu.json +41 -0
- package/translations/ro_RO.json +41 -0
- package/translations/ru_RU.json +41 -0
- package/translations/si.json +41 -0
- package/translations/sq.json +41 -0
- package/translations/sw.json +41 -0
- package/translations/sw_KE.json +41 -0
- package/translations/tr.json +41 -0
- package/translations/tr_TR.json +41 -0
- package/translations/uk.json +41 -0
- package/translations/uz.json +41 -0
- package/translations/uz@Latn.json +41 -0
- package/translations/uz_UZ.json +41 -0
- package/translations/vi.json +41 -0
- package/translations/zh.json +41 -0
- package/translations/zh_CN.json +41 -0
- package/translations/zh_TW.json +41 -0
- package/tsconfig.json +25 -0
- package/yarnrc.yml +14 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import {
|
|
4
|
+
openmrsFetch,
|
|
5
|
+
setSessionLocation,
|
|
6
|
+
setUserProperties,
|
|
7
|
+
showSnackbar,
|
|
8
|
+
useConfig,
|
|
9
|
+
useSession,
|
|
10
|
+
type LoggedInUser,
|
|
11
|
+
type Session,
|
|
12
|
+
type FetchResponse,
|
|
13
|
+
} from '@openmrs/esm-framework';
|
|
14
|
+
import {
|
|
15
|
+
mockLoginLocations,
|
|
16
|
+
validatingLocationFailureResponse,
|
|
17
|
+
validatingLocationSuccessResponse,
|
|
18
|
+
} from '../../__mocks__/locations.mock';
|
|
19
|
+
import { mockConfig } from '../../__mocks__/config.mock';
|
|
20
|
+
import renderWithRouter from '../test-helpers/render-with-router';
|
|
21
|
+
import LocationPickerView from './location-picker-view.component';
|
|
22
|
+
|
|
23
|
+
const fistLocation = {
|
|
24
|
+
uuid: 'uuid_1',
|
|
25
|
+
name: 'location_1',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const secondLocation = {
|
|
29
|
+
uuid: 'uuid_2',
|
|
30
|
+
name: 'location_2',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const invalidLocationUuid = '2gf1b7d4-c865-4178-82b0-5932e51503d6';
|
|
34
|
+
const userUuid = '90bd24b3-e700-46b0-a5ef-c85afdfededd';
|
|
35
|
+
|
|
36
|
+
const mockOpenmrsFetch = jest.mocked(openmrsFetch);
|
|
37
|
+
const mockUseConfig = jest.mocked(useConfig);
|
|
38
|
+
const mockUseSession = jest.mocked(useSession);
|
|
39
|
+
const mockSetSessionLocation = jest.mocked(setSessionLocation);
|
|
40
|
+
const mockSetUserProperties = jest.mocked(setUserProperties);
|
|
41
|
+
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
42
|
+
|
|
43
|
+
describe('LocationPickerView', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
mockUseConfig.mockReturnValue(mockConfig);
|
|
46
|
+
|
|
47
|
+
mockUseSession.mockReturnValue({
|
|
48
|
+
user: {
|
|
49
|
+
display: 'Testy McTesterface',
|
|
50
|
+
uuid: '90bd24b3-e700-46b0-a5ef-c85afdfededd',
|
|
51
|
+
userProperties: {},
|
|
52
|
+
} as LoggedInUser,
|
|
53
|
+
} as Session);
|
|
54
|
+
|
|
55
|
+
const urlResponseMap: Record<string, FetchResponse<unknown>> = {
|
|
56
|
+
[`/ws/fhir2/R4/Location?_id=${fistLocation.uuid}`]: validatingLocationSuccessResponse as FetchResponse<unknown>,
|
|
57
|
+
[`/ws/fhir2/R4/Location?_id=${invalidLocationUuid}`]: validatingLocationFailureResponse as FetchResponse<unknown>,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
mockOpenmrsFetch.mockImplementation(
|
|
61
|
+
async (url) => urlResponseMap[url] ?? (mockLoginLocations as FetchResponse<unknown>),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
mockSetSessionLocation.mockResolvedValue(undefined);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renders the welcome message and location selection form', () => {
|
|
68
|
+
renderWithRouter(LocationPickerView, {
|
|
69
|
+
currentLocationUuid: 'some-location-uuid',
|
|
70
|
+
hideWelcomeMessage: false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(screen.getByText(/welcome testy mctesterface/i)).toBeInTheDocument();
|
|
74
|
+
expect(
|
|
75
|
+
screen.getByText(/select your location from the list below. use the search bar to find your location/i),
|
|
76
|
+
).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('disables the confirm button when no location is selected', () => {
|
|
80
|
+
renderWithRouter(LocationPickerView, {});
|
|
81
|
+
|
|
82
|
+
const confirmButton = screen.getByRole('button', { name: /confirm/i });
|
|
83
|
+
expect(confirmButton).toBeDisabled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('enables the confirm button when a location is selected', async () => {
|
|
87
|
+
const user = userEvent.setup();
|
|
88
|
+
renderWithRouter(LocationPickerView, {});
|
|
89
|
+
|
|
90
|
+
const confirmButton = screen.getByRole('button', { name: /confirm/i });
|
|
91
|
+
expect(confirmButton).toBeDisabled();
|
|
92
|
+
|
|
93
|
+
const location = await screen.findByRole('radio', { name: fistLocation.name });
|
|
94
|
+
await user.click(location);
|
|
95
|
+
|
|
96
|
+
expect(confirmButton).toBeEnabled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('Saving location preference', () => {
|
|
100
|
+
it('allows user to save their preferred location for future logins', async () => {
|
|
101
|
+
const user = userEvent.setup();
|
|
102
|
+
|
|
103
|
+
renderWithRouter(LocationPickerView, {});
|
|
104
|
+
|
|
105
|
+
const location = await screen.findByRole('radio', { name: fistLocation.name });
|
|
106
|
+
const checkbox = screen.getByLabelText(/remember my location for future logins/i);
|
|
107
|
+
const submitButton = screen.getByRole('button', { name: /confirm/i });
|
|
108
|
+
|
|
109
|
+
await user.click(location);
|
|
110
|
+
expect(submitButton).toBeEnabled();
|
|
111
|
+
|
|
112
|
+
await user.click(checkbox);
|
|
113
|
+
expect(checkbox).toBeChecked();
|
|
114
|
+
|
|
115
|
+
await user.click(submitButton);
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(mockSetUserProperties).toHaveBeenCalledWith(userUuid, {
|
|
122
|
+
defaultLocation: fistLocation.uuid,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
127
|
+
expect.objectContaining({
|
|
128
|
+
kind: 'success',
|
|
129
|
+
title: 'Location saved',
|
|
130
|
+
subtitle: 'Your preferred location has been saved for future logins',
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('does not save preference when user submits without checking the checkbox', async () => {
|
|
137
|
+
const user = userEvent.setup();
|
|
138
|
+
|
|
139
|
+
renderWithRouter(LocationPickerView, {});
|
|
140
|
+
|
|
141
|
+
const location = await screen.findByRole('radio', { name: fistLocation.name });
|
|
142
|
+
const checkbox = screen.getByLabelText(/remember my location for future logins/i);
|
|
143
|
+
const submitButton = screen.getByRole('button', { name: /confirm/i });
|
|
144
|
+
|
|
145
|
+
await user.click(location);
|
|
146
|
+
expect(checkbox).not.toBeChecked();
|
|
147
|
+
|
|
148
|
+
await user.click(submitButton);
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(mockSetUserProperties).not.toHaveBeenCalled();
|
|
155
|
+
expect(mockShowSnackbar).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('automatically redirects when user has a valid saved location preference', async () => {
|
|
159
|
+
const validLocationUuid = fistLocation.uuid;
|
|
160
|
+
mockUseSession.mockReturnValue({
|
|
161
|
+
user: {
|
|
162
|
+
display: 'Testy McTesterface',
|
|
163
|
+
uuid: userUuid,
|
|
164
|
+
userProperties: {
|
|
165
|
+
defaultLocation: validLocationUuid,
|
|
166
|
+
},
|
|
167
|
+
} as LoggedInUser,
|
|
168
|
+
} as Session);
|
|
169
|
+
|
|
170
|
+
renderWithRouter(LocationPickerView, {});
|
|
171
|
+
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(mockSetSessionLocation).toHaveBeenCalledWith(validLocationUuid, expect.anything());
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(mockSetUserProperties).not.toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('shows location picker when saved location preference is invalid', async () => {
|
|
180
|
+
mockUseSession.mockReturnValue({
|
|
181
|
+
user: {
|
|
182
|
+
display: 'Testy McTesterface',
|
|
183
|
+
uuid: userUuid,
|
|
184
|
+
userProperties: {
|
|
185
|
+
defaultLocation: invalidLocationUuid,
|
|
186
|
+
},
|
|
187
|
+
} as LoggedInUser,
|
|
188
|
+
} as Session);
|
|
189
|
+
|
|
190
|
+
renderWithRouter(LocationPickerView, {});
|
|
191
|
+
|
|
192
|
+
const checkbox = screen.getByLabelText(/remember my location for future logins/i);
|
|
193
|
+
expect(checkbox).toBeChecked();
|
|
194
|
+
|
|
195
|
+
expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument();
|
|
196
|
+
expect(mockSetSessionLocation).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('Updating location preference', () => {
|
|
201
|
+
it('shows location picker when update=true is in URL params', () => {
|
|
202
|
+
mockUseSession.mockReturnValue({
|
|
203
|
+
user: {
|
|
204
|
+
display: 'Testy McTesterface',
|
|
205
|
+
uuid: userUuid,
|
|
206
|
+
userProperties: {
|
|
207
|
+
defaultLocation: fistLocation.uuid,
|
|
208
|
+
},
|
|
209
|
+
} as LoggedInUser,
|
|
210
|
+
} as Session);
|
|
211
|
+
|
|
212
|
+
renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
|
|
213
|
+
|
|
214
|
+
const checkbox = screen.getByLabelText(/remember my location for future logins/i);
|
|
215
|
+
expect(checkbox).toBeChecked();
|
|
216
|
+
|
|
217
|
+
expect(screen.getByRole('button', { name: /confirm/i })).toBeInTheDocument();
|
|
218
|
+
expect(mockSetSessionLocation).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('allows user to remove saved preference by unchecking the checkbox', async () => {
|
|
222
|
+
const user = userEvent.setup();
|
|
223
|
+
|
|
224
|
+
mockUseSession.mockReturnValue({
|
|
225
|
+
user: {
|
|
226
|
+
display: 'Testy McTesterface',
|
|
227
|
+
uuid: userUuid,
|
|
228
|
+
userProperties: {
|
|
229
|
+
defaultLocation: '1ce1b7d4-c865-4178-82b0-5932e51503d6',
|
|
230
|
+
},
|
|
231
|
+
} as LoggedInUser,
|
|
232
|
+
} as Session);
|
|
233
|
+
|
|
234
|
+
renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
|
|
235
|
+
|
|
236
|
+
const checkbox = screen.getByLabelText(/remember my location for future logins/i);
|
|
237
|
+
expect(checkbox).toBeChecked();
|
|
238
|
+
|
|
239
|
+
const location = screen.getByRole('radio', { name: fistLocation.name });
|
|
240
|
+
await user.click(location);
|
|
241
|
+
|
|
242
|
+
expect(mockSetSessionLocation).not.toHaveBeenCalled();
|
|
243
|
+
|
|
244
|
+
await user.click(checkbox);
|
|
245
|
+
expect(checkbox).not.toBeChecked();
|
|
246
|
+
|
|
247
|
+
const submitButton = screen.getByRole('button', { name: /confirm/i });
|
|
248
|
+
await user.click(submitButton);
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(mockSetUserProperties).toHaveBeenCalledWith(userUuid, {});
|
|
255
|
+
|
|
256
|
+
await waitFor(() => {
|
|
257
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
258
|
+
expect.objectContaining({
|
|
259
|
+
kind: 'success',
|
|
260
|
+
title: 'Location preference removed',
|
|
261
|
+
subtitle: 'You will need to select a location on each login',
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('allows user to update their preferred location', async () => {
|
|
268
|
+
const user = userEvent.setup();
|
|
269
|
+
|
|
270
|
+
mockUseSession.mockReturnValue({
|
|
271
|
+
user: {
|
|
272
|
+
display: 'Testy McTesterface',
|
|
273
|
+
uuid: userUuid,
|
|
274
|
+
userProperties: {
|
|
275
|
+
defaultLocation: fistLocation.uuid,
|
|
276
|
+
},
|
|
277
|
+
} as LoggedInUser,
|
|
278
|
+
} as Session);
|
|
279
|
+
|
|
280
|
+
renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
|
|
281
|
+
|
|
282
|
+
const checkbox = screen.getByLabelText(/remember my location for future logins/i);
|
|
283
|
+
expect(checkbox).toBeChecked();
|
|
284
|
+
|
|
285
|
+
const location = await screen.findByRole('radio', { name: secondLocation.name });
|
|
286
|
+
const submitButton = screen.getByRole('button', { name: /confirm/i });
|
|
287
|
+
|
|
288
|
+
await user.click(location);
|
|
289
|
+
await user.click(submitButton);
|
|
290
|
+
|
|
291
|
+
await waitFor(() => {
|
|
292
|
+
expect(mockSetSessionLocation).toHaveBeenCalledWith(secondLocation.uuid, expect.anything());
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(mockSetUserProperties).toHaveBeenCalledWith(userUuid, {
|
|
296
|
+
defaultLocation: secondLocation.uuid,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await waitFor(() => {
|
|
300
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
301
|
+
expect.objectContaining({
|
|
302
|
+
kind: 'success',
|
|
303
|
+
title: 'Location updated',
|
|
304
|
+
subtitle: 'Your preferred login location has been updated',
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('does not update preference when user selects the same location', async () => {
|
|
311
|
+
const user = userEvent.setup();
|
|
312
|
+
|
|
313
|
+
mockUseSession.mockReturnValue({
|
|
314
|
+
user: {
|
|
315
|
+
display: 'Testy McTesterface',
|
|
316
|
+
uuid: userUuid,
|
|
317
|
+
userProperties: {
|
|
318
|
+
defaultLocation: fistLocation.uuid,
|
|
319
|
+
},
|
|
320
|
+
} as LoggedInUser,
|
|
321
|
+
} as Session);
|
|
322
|
+
|
|
323
|
+
renderWithRouter(LocationPickerView, {}, { routes: ['?update=true'] });
|
|
324
|
+
|
|
325
|
+
const checkbox = screen.getByLabelText(/remember my location for future logins/i);
|
|
326
|
+
expect(checkbox).toBeChecked();
|
|
327
|
+
|
|
328
|
+
const location = await screen.findByRole('radio', { name: fistLocation.name });
|
|
329
|
+
const submitButton = screen.getByRole('button', { name: /confirm/i });
|
|
330
|
+
|
|
331
|
+
await user.click(location);
|
|
332
|
+
await user.click(submitButton);
|
|
333
|
+
|
|
334
|
+
await waitFor(() => {
|
|
335
|
+
expect(mockSetSessionLocation).toHaveBeenCalledWith(fistLocation.uuid, expect.anything());
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(mockSetUserProperties).not.toHaveBeenCalled();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Button, InlineLoading, InlineNotification, PasswordInput, TextInput, Tile } from '@carbon/react';
|
|
5
|
+
import {
|
|
6
|
+
ArrowRightIcon,
|
|
7
|
+
getCoreTranslation,
|
|
8
|
+
refetchCurrentUser,
|
|
9
|
+
navigate as openmrsNavigate,
|
|
10
|
+
useConfig,
|
|
11
|
+
useConnectivity,
|
|
12
|
+
useSession,
|
|
13
|
+
} from '@openmrs/esm-framework';
|
|
14
|
+
import { type ConfigSchema } from '../config-schema';
|
|
15
|
+
import Logo from '../logo.component';
|
|
16
|
+
import styles from './login.scss';
|
|
17
|
+
import { getEmail, getOtp } from '../resources/otp.resource';
|
|
18
|
+
import { getOtpEnabledStatus } from '../utils/get-base-url';
|
|
19
|
+
import image from '../assets/medicine.jpg';
|
|
20
|
+
import openmrsLogo from '../assets/openmrs.jpg';
|
|
21
|
+
import dhaLogo from '../assets/dha.png';
|
|
22
|
+
import gokLogo from '../assets/gok.png';
|
|
23
|
+
|
|
24
|
+
export interface LoginReferrer {
|
|
25
|
+
referrer?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Login: React.FC = () => {
|
|
29
|
+
const { showPasswordOnSeparateScreen, provider: loginProvider, links: loginLinks } = useConfig<ConfigSchema>();
|
|
30
|
+
const isLoginEnabled = useConnectivity();
|
|
31
|
+
const { t } = useTranslation();
|
|
32
|
+
const { user } = useSession();
|
|
33
|
+
const location = useLocation() as unknown as Omit<Location, 'state'> & {
|
|
34
|
+
state: LoginReferrer;
|
|
35
|
+
};
|
|
36
|
+
const navigate = useNavigate();
|
|
37
|
+
|
|
38
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
39
|
+
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
|
40
|
+
const [password, setPassword] = useState('');
|
|
41
|
+
const [username, setUsername] = useState('');
|
|
42
|
+
const [showPasswordField, setShowPasswordField] = useState(false);
|
|
43
|
+
const passwordInputRef = useRef<HTMLInputElement>(null);
|
|
44
|
+
const usernameInputRef = useRef<HTMLInputElement>(null);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!user) {
|
|
48
|
+
if (loginProvider.type === 'oauth2') {
|
|
49
|
+
openmrsNavigate({ to: loginProvider.loginUrl });
|
|
50
|
+
} else if (!username && location.pathname === '/login/confirm') {
|
|
51
|
+
navigate('/login');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}, [username, navigate, location, user, loginProvider]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (showPasswordOnSeparateScreen) {
|
|
58
|
+
if (showPasswordField) {
|
|
59
|
+
if (!passwordInputRef.current?.value) {
|
|
60
|
+
passwordInputRef.current?.focus();
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
usernameInputRef.current?.focus();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, [showPasswordField, showPasswordOnSeparateScreen]);
|
|
67
|
+
|
|
68
|
+
const continueLogin = useCallback(() => {
|
|
69
|
+
const currentUsername = usernameInputRef.current?.value?.trim();
|
|
70
|
+
if (currentUsername) {
|
|
71
|
+
// If credentials were autofilled, input onChange might not have been called
|
|
72
|
+
setUsername(currentUsername);
|
|
73
|
+
setShowPasswordField(true);
|
|
74
|
+
} else {
|
|
75
|
+
usernameInputRef.current?.focus();
|
|
76
|
+
}
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
const changeUsername = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setUsername(evt.target.value), []);
|
|
80
|
+
const changePassword = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setPassword(evt.target.value), []);
|
|
81
|
+
|
|
82
|
+
const handleSubmit = useCallback(
|
|
83
|
+
async (evt: React.FormEvent<HTMLFormElement>) => {
|
|
84
|
+
evt.preventDefault();
|
|
85
|
+
evt.stopPropagation();
|
|
86
|
+
|
|
87
|
+
// If credentials were autofilled, input onChange might not have been called
|
|
88
|
+
const currentUsername = usernameInputRef.current?.value?.trim() || username;
|
|
89
|
+
const currentPassword = passwordInputRef.current?.value || password;
|
|
90
|
+
const isOtpEnabled: boolean = await getOtpEnabledStatus();
|
|
91
|
+
|
|
92
|
+
if (showPasswordOnSeparateScreen && !showPasswordField) {
|
|
93
|
+
continueLogin();
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!currentPassword || !currentPassword.trim()) {
|
|
98
|
+
passwordInputRef.current?.focus();
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
setIsLoggingIn(true);
|
|
104
|
+
const sessionStore = await refetchCurrentUser(currentUsername, currentPassword);
|
|
105
|
+
const session = sessionStore.session;
|
|
106
|
+
const authenticated = sessionStore?.session?.authenticated;
|
|
107
|
+
|
|
108
|
+
if (isOtpEnabled === true) {
|
|
109
|
+
if (authenticated) {
|
|
110
|
+
if (session.sessionLocation) {
|
|
111
|
+
let to = loginLinks?.loginSuccess || '/home';
|
|
112
|
+
if (location?.state?.referrer) {
|
|
113
|
+
if (location.state.referrer.startsWith('/')) {
|
|
114
|
+
to = `\${openmrsSpaBase}${location.state.referrer}`;
|
|
115
|
+
} else {
|
|
116
|
+
to = location.state.referrer;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const uuid = session.user.person.uuid;
|
|
120
|
+
try {
|
|
121
|
+
const email = await getEmail(uuid, username, password);
|
|
122
|
+
await getOtp(username, password, email);
|
|
123
|
+
navigate('otp', {
|
|
124
|
+
state: {
|
|
125
|
+
username,
|
|
126
|
+
password,
|
|
127
|
+
referrer: location?.state?.referrer,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
} catch (err: any) {
|
|
131
|
+
setErrorMessage(err.message);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
} else if (!session.sessionLocation) {
|
|
135
|
+
const uuid = session.user.person.uuid;
|
|
136
|
+
try {
|
|
137
|
+
await getOtp(username, password, uuid);
|
|
138
|
+
navigate('otp', {
|
|
139
|
+
state: {
|
|
140
|
+
username,
|
|
141
|
+
password,
|
|
142
|
+
referrer: location?.state?.referrer,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
} catch (err: any) {
|
|
146
|
+
setErrorMessage(err.message);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
|
|
152
|
+
setUsername('');
|
|
153
|
+
setPassword('');
|
|
154
|
+
if (showPasswordOnSeparateScreen) {
|
|
155
|
+
setShowPasswordField(false);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
if (authenticated) {
|
|
160
|
+
if (session.sessionLocation) {
|
|
161
|
+
let to = loginLinks?.loginSuccess || '/home';
|
|
162
|
+
if (location?.state?.referrer) {
|
|
163
|
+
if (location.state.referrer.startsWith('/')) {
|
|
164
|
+
to = `\${openmrsSpaBase}${location.state.referrer}`;
|
|
165
|
+
} else {
|
|
166
|
+
to = location.state.referrer;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
openmrsNavigate({ to });
|
|
171
|
+
} else {
|
|
172
|
+
navigate('/login/location');
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
|
|
176
|
+
setUsername('');
|
|
177
|
+
setPassword('');
|
|
178
|
+
if (showPasswordOnSeparateScreen) {
|
|
179
|
+
setShowPasswordField(false);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return true;
|
|
185
|
+
} catch (error: unknown) {
|
|
186
|
+
if (error instanceof Error) {
|
|
187
|
+
setErrorMessage(error.message);
|
|
188
|
+
} else {
|
|
189
|
+
setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
|
|
190
|
+
}
|
|
191
|
+
setUsername('');
|
|
192
|
+
setPassword('');
|
|
193
|
+
if (showPasswordOnSeparateScreen) {
|
|
194
|
+
setShowPasswordField(false);
|
|
195
|
+
}
|
|
196
|
+
} finally {
|
|
197
|
+
setIsLoggingIn(false);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
[
|
|
201
|
+
username,
|
|
202
|
+
password,
|
|
203
|
+
navigate,
|
|
204
|
+
showPasswordOnSeparateScreen,
|
|
205
|
+
showPasswordField,
|
|
206
|
+
loginLinks,
|
|
207
|
+
location,
|
|
208
|
+
t,
|
|
209
|
+
continueLogin,
|
|
210
|
+
],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (!loginProvider || loginProvider.type === 'basic') {
|
|
214
|
+
return (
|
|
215
|
+
<>
|
|
216
|
+
<div className={styles.wrapperContainer}>
|
|
217
|
+
<div className={styles.logoContainer}>
|
|
218
|
+
<div className={styles.container}>
|
|
219
|
+
<Tile className={styles.loginCard}>
|
|
220
|
+
{errorMessage && (
|
|
221
|
+
<div className={styles.errorMessage}>
|
|
222
|
+
<InlineNotification
|
|
223
|
+
kind="error"
|
|
224
|
+
subtitle={errorMessage}
|
|
225
|
+
title={getCoreTranslation('error')}
|
|
226
|
+
onClick={() => setErrorMessage('')}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
<div className={styles.center}>
|
|
231
|
+
<Logo t={t} />
|
|
232
|
+
</div>
|
|
233
|
+
<form onSubmit={handleSubmit}>
|
|
234
|
+
<div className={styles.inputGroup}>
|
|
235
|
+
<TextInput
|
|
236
|
+
id="username"
|
|
237
|
+
type="text"
|
|
238
|
+
name="username"
|
|
239
|
+
autoComplete="username"
|
|
240
|
+
labelText={t('username', 'Username')}
|
|
241
|
+
value={username}
|
|
242
|
+
onChange={changeUsername}
|
|
243
|
+
ref={usernameInputRef}
|
|
244
|
+
required
|
|
245
|
+
autoFocus
|
|
246
|
+
/>
|
|
247
|
+
{showPasswordOnSeparateScreen ? (
|
|
248
|
+
<>
|
|
249
|
+
<div className={showPasswordField ? undefined : styles.hiddenPasswordField}>
|
|
250
|
+
<PasswordInput
|
|
251
|
+
id="password"
|
|
252
|
+
labelText={t('password', 'Password')}
|
|
253
|
+
name="password"
|
|
254
|
+
autoComplete="current-password"
|
|
255
|
+
onChange={changePassword}
|
|
256
|
+
ref={passwordInputRef}
|
|
257
|
+
required
|
|
258
|
+
value={password}
|
|
259
|
+
showPasswordLabel={t('showPassword', 'Show password')}
|
|
260
|
+
invalidText={t('validValueRequired', 'A valid value is required')}
|
|
261
|
+
aria-hidden={!showPasswordField}
|
|
262
|
+
tabIndex={showPasswordField ? 0 : -1}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
{showPasswordField ? (
|
|
266
|
+
<Button
|
|
267
|
+
type="submit"
|
|
268
|
+
className={styles.continueButton}
|
|
269
|
+
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
|
|
270
|
+
iconDescription={t('loginButtonIconDescription', 'Log in button')}
|
|
271
|
+
disabled={!isLoginEnabled || isLoggingIn}
|
|
272
|
+
>
|
|
273
|
+
{isLoggingIn ? (
|
|
274
|
+
<InlineLoading
|
|
275
|
+
className={styles.loader}
|
|
276
|
+
description={t('loggingIn', 'Logging in') + '...'}
|
|
277
|
+
/>
|
|
278
|
+
) : (
|
|
279
|
+
t('login', 'Log in')
|
|
280
|
+
)}
|
|
281
|
+
</Button>
|
|
282
|
+
) : (
|
|
283
|
+
<Button
|
|
284
|
+
type="submit"
|
|
285
|
+
className={styles.continueButton}
|
|
286
|
+
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
|
|
287
|
+
iconDescription="Continue to password"
|
|
288
|
+
onClick={(evt) => {
|
|
289
|
+
evt.preventDefault();
|
|
290
|
+
continueLogin();
|
|
291
|
+
}}
|
|
292
|
+
disabled={!isLoginEnabled}
|
|
293
|
+
>
|
|
294
|
+
{t('continue', 'Continue')}
|
|
295
|
+
</Button>
|
|
296
|
+
)}
|
|
297
|
+
</>
|
|
298
|
+
) : (
|
|
299
|
+
<>
|
|
300
|
+
<PasswordInput
|
|
301
|
+
id="password"
|
|
302
|
+
labelText={t('password', 'Password')}
|
|
303
|
+
name="password"
|
|
304
|
+
autoComplete="current-password"
|
|
305
|
+
onChange={changePassword}
|
|
306
|
+
ref={passwordInputRef}
|
|
307
|
+
required
|
|
308
|
+
value={password}
|
|
309
|
+
showPasswordLabel={t('showPassword', 'Show password')}
|
|
310
|
+
invalidText={t('validValueRequired', 'A valid value is required')}
|
|
311
|
+
/>
|
|
312
|
+
<Button
|
|
313
|
+
type="submit"
|
|
314
|
+
className={styles.continueButton}
|
|
315
|
+
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
|
|
316
|
+
iconDescription="Log in"
|
|
317
|
+
disabled={!isLoginEnabled || isLoggingIn}
|
|
318
|
+
>
|
|
319
|
+
{isLoggingIn ? (
|
|
320
|
+
<InlineLoading
|
|
321
|
+
className={styles.loader}
|
|
322
|
+
description={t('loggingIn', 'Logging in') + '...'}
|
|
323
|
+
/>
|
|
324
|
+
) : (
|
|
325
|
+
t('login', 'Log in')
|
|
326
|
+
)}
|
|
327
|
+
</Button>
|
|
328
|
+
</>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
</form>
|
|
332
|
+
</Tile>
|
|
333
|
+
</div>
|
|
334
|
+
<div className={styles.logoSection}>
|
|
335
|
+
<div className={styles.leftLogos}>
|
|
336
|
+
<img src={gokLogo} alt="GOK logo" className={styles.supportingLogos} />
|
|
337
|
+
<img src={dhaLogo} alt="DHA logo" className={styles.dhaLogo} />
|
|
338
|
+
</div>
|
|
339
|
+
<img src={openmrsLogo} alt="OpenMRS logo" className={styles.openmsrsLogo} />
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
<img className={styles.image} src={image} alt="TAIFA CARE" />
|
|
343
|
+
</div>
|
|
344
|
+
</>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
export default Login;
|