-
Notifications
You must be signed in to change notification settings - Fork 208
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(feat) O3-3423: Allow user to change password (#1047)
Co-authored-by: gitcliff <[email protected]>
- Loading branch information
Showing
17 changed files
with
327 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -76,5 +76,10 @@ | |
"@carbon/icons-react": "11.37.0", | ||
"minipass": "3.3.5" | ||
}, | ||
"packageManager": "[email protected]" | ||
"packageManager": "[email protected]", | ||
"dependencies": { | ||
"@hookform/resolvers": "^3.6.0", | ||
"react-hook-form": "^7.52.0", | ||
"zod": "^3.23.8" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
packages/apps/esm-login-app/src/change-password/change-password-link.extension.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import React, { useCallback } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Button, SwitcherItem } from '@carbon/react'; | ||
import { PasswordIcon, showModal } from '@openmrs/esm-framework'; | ||
import styles from './change-password.scss'; | ||
|
||
const ChangePasswordLink: React.FC = () => { | ||
const { t } = useTranslation(); | ||
|
||
const launchChangePasswordModal = useCallback(() => showModal('change-password-modal'), []); | ||
|
||
return ( | ||
<SwitcherItem aria-label={t('changePassword', 'ChangePassword')} className={styles.panelItemContainer}> | ||
<div> | ||
<PasswordIcon size={20} /> | ||
<p>{t('password', 'Password')}</p> | ||
</div> | ||
<Button kind="ghost" onClick={launchChangePasswordModal}> | ||
{t('change', 'Change')} | ||
</Button> | ||
</SwitcherItem> | ||
); | ||
}; | ||
|
||
export default ChangePasswordLink; |
24 changes: 24 additions & 0 deletions
24
packages/apps/esm-login-app/src/change-password/change-password-link.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import React from 'react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { render, screen } from '@testing-library/react'; | ||
import { showModal } from '@openmrs/esm-framework'; | ||
import ChangePasswordLink from './change-password-link.extension'; | ||
|
||
const mockShowModal = jest.mocked(showModal); | ||
|
||
describe('<ChangePasswordLink/>', () => { | ||
it('should display the `Change password` link', async () => { | ||
const user = userEvent.setup(); | ||
|
||
render(<ChangePasswordLink />); | ||
|
||
const changePasswordLink = screen.getByRole('button', { | ||
name: /Change/i, | ||
}); | ||
|
||
await user.click(changePasswordLink); | ||
|
||
expect(mockShowModal).toHaveBeenCalledTimes(1); | ||
expect(mockShowModal).toHaveBeenCalledWith('change-password-modal'); | ||
}); | ||
}); |
9 changes: 9 additions & 0 deletions
9
packages/apps/esm-login-app/src/change-password/change-password-modal.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.submitButton { | ||
:global(.cds--inline-loading) { | ||
min-height: 1rem; | ||
} | ||
|
||
:global(.cds--inline-loading__text) { | ||
font-size: unset; | ||
} | ||
} |
142 changes: 142 additions & 0 deletions
142
packages/apps/esm-login-app/src/change-password/change-password.modal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import React, { useCallback, useState } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { z } from 'zod'; | ||
import { zodResolver } from '@hookform/resolvers/zod'; | ||
import { Controller, FormProvider, useForm, type SubmitHandler } from 'react-hook-form'; | ||
import { Button, Form, PasswordInput, InlineLoading, ModalBody, ModalFooter, ModalHeader, Stack } from '@carbon/react'; | ||
import { showSnackbar } from '@openmrs/esm-framework'; | ||
import { changeUserPassword } from './change-password.resource'; | ||
import styles from './change-password-modal.scss'; | ||
|
||
interface ChangePasswordModalProps { | ||
close(): () => void; | ||
} | ||
|
||
const ChangePasswordModal: React.FC<ChangePasswordModalProps> = ({ close }) => { | ||
const { t } = useTranslation(); | ||
const [isChangingPassword, setIsChangingPassword] = useState(false); | ||
|
||
const oldPasswordValidation = z.string({ | ||
required_error: t('oldPasswordRequired', 'Old password is required'), | ||
}); | ||
|
||
const newPasswordValidation = z.string({ | ||
required_error: t('newPasswordRequired', 'New password is required'), | ||
}); | ||
|
||
const passwordConfirmationValidation = z.string({ | ||
required_error: t('passwordConfirmationRequired', 'Password confirmation is required'), | ||
}); | ||
|
||
const changePasswordFormSchema = z | ||
.object({ | ||
oldPassword: oldPasswordValidation, | ||
newPassword: newPasswordValidation, | ||
passwordConfirmation: passwordConfirmationValidation, | ||
}) | ||
.refine((data) => data.newPassword === data.passwordConfirmation, { | ||
message: t('passwordsDoNotMatch', 'Passwords do not match'), | ||
path: ['passwordConfirmation'], | ||
}); | ||
|
||
const methods = useForm<z.infer<typeof changePasswordFormSchema>>({ | ||
mode: 'all', | ||
resolver: zodResolver(changePasswordFormSchema), | ||
}); | ||
|
||
const onSubmit: SubmitHandler<z.infer<typeof changePasswordFormSchema>> = useCallback((data) => { | ||
setIsChangingPassword(true); | ||
|
||
const { oldPassword, newPassword } = data; | ||
|
||
changeUserPassword(oldPassword, newPassword) | ||
.then(() => { | ||
close(); | ||
|
||
showSnackbar({ | ||
title: t('passwordChangedSuccessfully', 'Password changed successfully'), | ||
kind: 'success', | ||
}); | ||
}) | ||
.catch((error) => { | ||
showSnackbar({ | ||
kind: 'error', | ||
subtitle: error?.message, | ||
title: t('errorChangingPassword', 'Error changing password'), | ||
}); | ||
}) | ||
.finally(() => { | ||
setIsChangingPassword(false); | ||
}); | ||
}, []); | ||
|
||
const onError = () => setIsChangingPassword(false); | ||
|
||
return ( | ||
<FormProvider {...methods}> | ||
<Form onSubmit={methods.handleSubmit(onSubmit, onError)}> | ||
<ModalHeader closeModal={close} title={t('changePassword', 'Change password')} /> | ||
<ModalBody> | ||
<Stack gap={5} className={styles.languageOptionsContainer}> | ||
<Controller | ||
name="oldPassword" | ||
control={methods.control} | ||
render={({ field: { onChange, value }, fieldState: { error } }) => ( | ||
<PasswordInput | ||
id="oldPassword" | ||
invalid={!!error} | ||
invalidText={error?.message} | ||
labelText={t('oldPassword', 'Old password')} | ||
onChange={onChange} | ||
value={value} | ||
/> | ||
)} | ||
/> | ||
<Controller | ||
name="newPassword" | ||
control={methods.control} | ||
render={({ field: { onChange, value }, fieldState: { error } }) => ( | ||
<PasswordInput | ||
id="newPassword" | ||
invalid={!!error} | ||
invalidText={error?.message} | ||
labelText={t('newPassword', 'New password')} | ||
onChange={onChange} | ||
value={value} | ||
/> | ||
)} | ||
/> | ||
<Controller | ||
name="passwordConfirmation" | ||
control={methods.control} | ||
render={({ field: { onChange, value }, fieldState: { error } }) => ( | ||
<PasswordInput | ||
id="passwordConfirmation" | ||
invalid={!!error} | ||
invalidText={error?.message} | ||
labelText={t('confirmPassword', 'Confirm new password')} | ||
onChange={onChange} | ||
value={value} | ||
/> | ||
)} | ||
/> | ||
</Stack> | ||
</ModalBody> | ||
<ModalFooter> | ||
<Button kind="secondary" onClick={close}> | ||
{t('cancel', 'Cancel')} | ||
</Button> | ||
<Button className={styles.submitButton} disabled={isChangingPassword} type="submit"> | ||
{isChangingPassword ? ( | ||
<InlineLoading description={t('changingLanguage', 'Changing password') + '...'} /> | ||
) : ( | ||
<span>{t('change', 'Change')}</span> | ||
)} | ||
</Button> | ||
</ModalFooter> | ||
</Form> | ||
</FormProvider> | ||
); | ||
}; | ||
|
||
export default ChangePasswordModal; |
12 changes: 12 additions & 0 deletions
12
packages/apps/esm-login-app/src/change-password/change-password.resource.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; | ||
|
||
export function changeUserPassword(oldPassword: string, newPassword: string) { | ||
return openmrsFetch(`${restBaseUrl}/password`, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: { | ||
oldPassword, | ||
newPassword, | ||
}, | ||
}); | ||
} |
15 changes: 15 additions & 0 deletions
15
packages/apps/esm-login-app/src/change-password/change-password.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
.alignCenter { | ||
display: flex; | ||
text-align: center; | ||
} | ||
|
||
.panelItemContainer { | ||
a { | ||
@extend .alignCenter; | ||
justify-content: space-between; | ||
} | ||
|
||
div { | ||
@extend .alignCenter; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
01a0b40
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ibacher looks like the minified React error causing the E2E test failure is related to the icons package.
01a0b40
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's an on-going issue. The E2E tests never have the latest framework changes until they are merged in and the icon we use was added in this PR.