Skip to content

Commit cb4692f

Browse files
committed
rerun build
1 parent d583b58 commit cb4692f

File tree

11 files changed

+357
-2
lines changed

11 files changed

+357
-2
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React, { useCallback } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { SwitcherItem } from '@carbon/react';
4+
import { Settings } from '@carbon/react/icons';
5+
import { showModal } from '@openmrs/esm-framework';
6+
import styles from './change-password-button.scss';
7+
8+
export interface ChangePasswordLinkProps {}
9+
10+
const ChangePasswordLink: React.FC<ChangePasswordLinkProps> = () => {
11+
const { t } = useTranslation();
12+
13+
const launchChangePasswordModal = useCallback(() => {
14+
showModal('change-password-modal');
15+
}, []);
16+
17+
return (
18+
<>
19+
<SwitcherItem aria-label="Switcher Container">
20+
<div className={styles.changePasswordButton} role="button" onClick={launchChangePasswordModal} tabIndex={0}>
21+
<Settings size={20} />
22+
<p>{t('changePassword', 'Change Password')}</p>
23+
</div>
24+
</SwitcherItem>
25+
</>
26+
);
27+
};
28+
29+
export default ChangePasswordLink;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.changePasswordButton {
2+
width: 16rem;
3+
display: inline-flex;
4+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import ChangePasswordButton from './change-password-button.component';
3+
import { render, screen } from '@testing-library/react';
4+
import { showModal } from '@openmrs/esm-framework';
5+
import userEvent from '@testing-library/user-event';
6+
7+
const showModalMock = showModal as jest.Mock;
8+
9+
describe('<ChangePasswordButton/>', () => {
10+
beforeEach(() => {
11+
render(<ChangePasswordButton />);
12+
});
13+
14+
it('should display the `Change Password` button', async () => {
15+
const user = userEvent.setup();
16+
const changePasswordButton = await screen.findByRole('button', {
17+
name: /Change Password/i,
18+
});
19+
20+
await user.click(changePasswordButton);
21+
22+
expect(showModalMock).toHaveBeenCalledWith('change-password-modal');
23+
});
24+
});
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import React, { useCallback, useEffect, useRef, useState } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { Button, ModalBody, ModalFooter, ModalHeader, Tile, PasswordInput, Form, Layer } from '@carbon/react';
4+
import { navigate, showSnackbar } from '@openmrs/esm-framework';
5+
import styles from './change-password-modal.scss';
6+
import { performPasswordChange } from './change-password-model.resource';
7+
8+
interface ChangePasswordModalProps {
9+
close(): void;
10+
}
11+
12+
export default function ChangePasswordModal({ close }: ChangePasswordModalProps) {
13+
const { t } = useTranslation();
14+
const [isSavingPassword, setIsSavingPassword] = useState(false);
15+
const oldPasswordInputRef = useRef<HTMLInputElement>(null);
16+
const newPasswordInputRef = useRef<HTMLInputElement>(null);
17+
const confirmPasswordInputRef = useRef<HTMLInputElement>(null);
18+
const formRef = useRef<HTMLFormElement>(null);
19+
const [newPasswordError, setNewPasswordErr] = useState('');
20+
const [oldPasswordError, setOldPasswordErr] = useState('');
21+
const [confirmPasswordError, setConfirmPasswordError] = useState('');
22+
const [isOldPasswordInvalid, setIsOldPasswordInvalid] = useState<boolean>(true);
23+
const [isNewPasswordInvalid, setIsNewPasswordInvalid] = useState<boolean>(true);
24+
const [isConfirmPasswordInvalid, setIsConfirmPasswordInvalid] = useState<boolean>(true);
25+
const [passwordInput, setPasswordInput] = useState({
26+
oldPassword: '',
27+
newPassword: '',
28+
confirmPassword: '',
29+
});
30+
31+
const handleValidation = useCallback(
32+
(passwordInputValue, passwordInputFieldName) => {
33+
if (passwordInputFieldName === 'newPassword') {
34+
const uppercaseRegExp = /(?=.*?[A-Z])/;
35+
const lowercaseRegExp = /(?=.*?[a-z])/;
36+
const digitsRegExp = /(?=.*?[0-9])/;
37+
const minLengthRegExp = /.{8,}/;
38+
const passwordLength = passwordInputValue.length;
39+
const uppercasePassword = uppercaseRegExp.test(passwordInputValue);
40+
const lowercasePassword = lowercaseRegExp.test(passwordInputValue);
41+
const digitsPassword = digitsRegExp.test(passwordInputValue);
42+
const minLengthPassword = minLengthRegExp.test(passwordInputValue);
43+
let errMsg = '';
44+
if (passwordLength === 0) {
45+
errMsg = t('passwordIsEmpty', 'Password is empty');
46+
} else if (!uppercasePassword) {
47+
errMsg = t('atLeastOneUppercase', 'At least one Uppercase');
48+
} else if (!lowercasePassword) {
49+
errMsg = t('atLeastOneLowercase', 'At least one Lowercase');
50+
} else if (!digitsPassword) {
51+
errMsg = t('atLeastOneDigit', 'At least one digit');
52+
} else if (!minLengthPassword) {
53+
errMsg = t('minimum8Characters', 'Minimum 8 characters');
54+
} else if (passwordInput.oldPassword.length > 0 && passwordInput.newPassword === passwordInput.oldPassword) {
55+
errMsg = t('newPasswordMustNotBeTheSameAsOld', 'New password must not be the same as password');
56+
} else {
57+
errMsg = '';
58+
setIsNewPasswordInvalid(false);
59+
}
60+
setNewPasswordErr(errMsg);
61+
} else if (
62+
passwordInputFieldName === 'confirmPassword' ||
63+
(passwordInputFieldName === 'newPassword' && passwordInput.confirmPassword.length > 0)
64+
) {
65+
if (passwordInput.confirmPassword !== passwordInput.newPassword) {
66+
setConfirmPasswordError(t('confirmPasswordMustBeTheSameAsNew', 'Confirm password must be the same as new'));
67+
} else {
68+
setConfirmPasswordError('');
69+
setIsConfirmPasswordInvalid(false);
70+
}
71+
} else {
72+
if (passwordInput.newPassword.length > 0 && passwordInput.newPassword === passwordInput.oldPassword) {
73+
setOldPasswordErr(t('oldPasswordMustNotBeTheSameAsNew', 'Old password must not be the same as new'));
74+
} else {
75+
setOldPasswordErr('');
76+
setIsOldPasswordInvalid(false);
77+
}
78+
}
79+
},
80+
[passwordInput.confirmPassword, passwordInput.newPassword, passwordInput.oldPassword, t],
81+
);
82+
83+
useEffect(() => {
84+
if (passwordInput.oldPassword !== '') {
85+
handleValidation(passwordInput.oldPassword, 'oldPassword');
86+
}
87+
if (passwordInput.newPassword !== '') {
88+
handleValidation(passwordInput.newPassword, 'newPassword');
89+
}
90+
if (passwordInput.confirmPassword !== '') {
91+
handleValidation(passwordInput.confirmPassword, 'confirmPassword');
92+
}
93+
}, [handleValidation, passwordInput]);
94+
95+
const handlePasswordChange = (event) => {
96+
const passwordInputValue = event.target.value.trim();
97+
const passwordInputFieldName = event.target.name;
98+
const NewPasswordInput = { ...passwordInput, [passwordInputFieldName]: passwordInputValue };
99+
setPasswordInput(NewPasswordInput);
100+
};
101+
102+
const handleSubmit = useCallback(
103+
async (evt: React.FormEvent<HTMLFormElement>) => {
104+
evt.preventDefault();
105+
setIsSavingPassword(true);
106+
performPasswordChange(passwordInput.oldPassword, passwordInput.confirmPassword)
107+
.then(() => {
108+
close();
109+
navigate({ to: `\${openmrsSpaBase}/logout` });
110+
showSnackbar({
111+
isLowContrast: true,
112+
kind: 'success',
113+
subtitle: t('userPasswordUpdated', 'User password updated successfully'),
114+
title: t('userPassword', 'User password'),
115+
});
116+
})
117+
.catch((error) => {
118+
setIsSavingPassword(false);
119+
showSnackbar({
120+
title: t('invalidPasswordCredentials', 'Invalid password provided'),
121+
kind: 'error',
122+
isLowContrast: false,
123+
subtitle: error?.message,
124+
});
125+
});
126+
},
127+
[close, passwordInput.confirmPassword, passwordInput.oldPassword, t],
128+
);
129+
130+
return (
131+
<>
132+
<ModalHeader closeModal={close} title={t('changePassword', 'Change Password')} />
133+
<ModalBody>
134+
<Tile>
135+
<Form onSubmit={handleSubmit} ref={formRef}>
136+
<div className={styles['input-group']}>
137+
<Layer>
138+
<PasswordInput
139+
id="oldPassword"
140+
invalid={oldPasswordError.length > 0}
141+
invalidText={oldPasswordError}
142+
labelText={t('oldPassword', 'Old Password')}
143+
name="oldPassword"
144+
value={passwordInput.oldPassword}
145+
onChange={handlePasswordChange}
146+
ref={oldPasswordInputRef}
147+
required
148+
showPasswordLabel="Show old password"
149+
/>
150+
</Layer>
151+
<Layer>
152+
<PasswordInput
153+
id="newPassword"
154+
invalid={newPasswordError.length > 0}
155+
invalidText={newPasswordError}
156+
labelText={t('newPassword', 'New Password')}
157+
name="newPassword"
158+
value={passwordInput.newPassword}
159+
onChange={handlePasswordChange}
160+
ref={newPasswordInputRef}
161+
required
162+
showPasswordLabel="Show new password"
163+
/>
164+
</Layer>
165+
<Layer>
166+
<PasswordInput
167+
id="confirmPassword"
168+
invalid={confirmPasswordError.length > 0}
169+
invalidText={confirmPasswordError}
170+
labelText={t('confirmPassword', 'Confirm Password')}
171+
name="confirmPassword"
172+
value={passwordInput.confirmPassword}
173+
onChange={handlePasswordChange}
174+
ref={confirmPasswordInputRef}
175+
required
176+
showPasswordLabel="Show confirm password"
177+
/>
178+
</Layer>
179+
</div>
180+
</Form>
181+
</Tile>
182+
</ModalBody>
183+
<ModalFooter>
184+
<Button kind="secondary" onClick={close}>
185+
{t('cancel', 'Cancel')}
186+
</Button>
187+
<Button
188+
type="submit"
189+
onClick={handleSubmit}
190+
disabled={isSavingPassword || isNewPasswordInvalid || isConfirmPasswordInvalid || isOldPasswordInvalid}
191+
>
192+
{t('updatePassword', 'Update Password')}
193+
</Button>
194+
</ModalFooter>
195+
</>
196+
);
197+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.input-group {
2+
:global(.cds--label) {
3+
margin-top: 1rem;
4+
}
5+
6+
:global(.cds--text-input) {
7+
background-color: white;
8+
}
9+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react';
3+
import ChangePasswordModal from './change-password-modal.component';
4+
import userEvent from '@testing-library/user-event';
5+
6+
describe(`Change Password Modal`, () => {
7+
it('should change user locale', async () => {
8+
const user = userEvent.setup();
9+
10+
render(<ChangePasswordModal close={jest.fn()} />);
11+
expect(screen.getByRole('button', { name: /Apply/ })).toBeDisabled();
12+
13+
await user.type(screen.getByLabelText(/Old Password/i), 'my-password');
14+
await user.type(screen.getByLabelText(/New Password/i), 'my-password');
15+
await user.type(screen.getByLabelText(/Confirm Password/i), 'my-password');
16+
await user.click(screen.getByRole('button', { name: /Apply/i }));
17+
});
18+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { openmrsFetch } from '@openmrs/esm-framework';
2+
3+
export async function performPasswordChange(oldPassword: string, newPassword: string) {
4+
return openmrsFetch(`/ws/rest/v1/password`, {
5+
headers: {
6+
'Content-Type': 'application/json',
7+
},
8+
method: 'POST',
9+
body: {
10+
oldPassword: oldPassword,
11+
newPassword: newPassword,
12+
},
13+
}).then((res) => {
14+
return res;
15+
});
16+
}

packages/esm-system-admin-app/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,13 @@ export const legacySystemAdminPageCardLink = getAsyncLifecycle(
3434
() => import('./dashboard/legacy-admin-page-link.component'),
3535
options,
3636
);
37+
38+
export const changePasswordButton = getAsyncLifecycle(
39+
() => import('./change-password-button/change-password-button.component'),
40+
options,
41+
);
42+
43+
export const changePasswordModal = getAsyncLifecycle(
44+
() => import('./change-password-modal/change-password-modal.component'),
45+
options,
46+
);

packages/esm-system-admin-app/src/routes.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@
1515
"slot": "system-admin-page-card-link-slot",
1616
"component": "legacySystemAdminPageCardLink",
1717
"order": 0
18+
},
19+
{
20+
"name": "change-password",
21+
"slot": "user-panel-slot",
22+
"component": "changePasswordButton",
23+
"online": true,
24+
"offline": true,
25+
"order": 3
26+
},
27+
{
28+
"name": "change-password-modal",
29+
"component": "changePasswordModal",
30+
"online": true,
31+
"offline": true
1832
}
1933
],
2034
"pages": [
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
{
2+
"atLeastOneDigit": "رقم واحد على الأقل",
3+
"atLeastOneLowercase": "حرف صغير واحد على الأقل",
4+
"atLeastOneUppercase": "حرف كبير واحد على الأقل",
5+
"cancel": "يلغي",
6+
"changePassword": "تغيير كلمة المرور",
27
"config": "الإعدادات",
8+
"confirmPassword": "تأكيد كلمة المرور",
9+
"confirmPasswordMustBeTheSameAsNew": "تأكيد كلمة المرور يجب أن تكون هي نفسها الجديدة",
10+
"invalidPasswordCredentials": "",
311
"legacyAdmin": "إدارة النظام القديم",
4-
"systemAdmin": "إدارة النظام"
12+
"minimum8Characters": "الحد الأدنى 8 أحرف",
13+
"newPassword": "كلمة المرور الجديدة",
14+
"newPasswordMustNotBeTheSameAsOld": "كلمة المرور الجديدة يجب ألا تكون نفس كلمة المرور",
15+
"oldPassword": "كلمة المرور القديمة",
16+
"oldPasswordMustNotBeTheSameAsNew": "يجب ألا تكون كلمة المرور القديمة هي نفس كلمة المرور الجديدة",
17+
"passwordIsEmpty": "كلمة المرور فارغة",
18+
"systemAdmin": "إدارة النظام",
19+
"updatePassword": "تطوير كلمة السر",
20+
"userPassword": "كلمة مرور المستخدم",
21+
"userPasswordUpdated": "تم تحديث كلمة مرور المستخدم بنجاح"
522
}

0 commit comments

Comments
 (0)