Skip to content

Commit

Permalink
(feat) O3-3537: Add a change-password page to the login-app (#1076)
Browse files Browse the repository at this point in the history
  • Loading branch information
miirochristopher authored and ibacher committed Jul 22, 2024
1 parent b089fa0 commit 6bad1ba
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { Controller, useForm, type SubmitHandler } from 'react-hook-form';
import { Button, Form, PasswordInput, InlineLoading, Tile } from '@carbon/react';
import { showSnackbar } from '@openmrs/esm-framework';
import { changeUserPassword } from './change-password.resource';
import Logo from '../logo.component';
import styles from './change-password.scss';

const ChangePassword: React.FC = () => {
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 {
handleSubmit,
control,
formState: { errors },
} = useForm({
resolver: zodResolver(changePasswordFormSchema),
});

const onSubmit: SubmitHandler<z.infer<typeof changePasswordFormSchema>> = useCallback((data) => {
setIsChangingPassword(true);

const { oldPassword, newPassword } = data;

changeUserPassword(oldPassword, newPassword)
.then(() => {
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 = useCallback(() => setIsChangingPassword(false), []);

return (
<div className={styles.container}>
<Tile className={styles.changePasswordCard}>
<div className={styles.alignCenter}>
<Logo t={t} />
</div>
<Form onSubmit={handleSubmit(onSubmit, onError)}>
<Controller
name="oldPassword"
control={control}
render={({ field: { onChange, value } }) => (
<PasswordInput
id="oldPassword"
invalid={!!errors?.oldPassword}
invalidText={errors?.oldPassword?.message}
labelText={t('oldPassword', 'Old password')}
onChange={onChange}
value={value}
/>
)}
/>
<Controller
name="newPassword"
control={control}
render={({ field: { onChange, value } }) => (
<PasswordInput
id="newPassword"
invalid={!!errors?.newPassword}
invalidText={errors?.newPassword?.message}
labelText={t('newPassword', 'New password')}
onChange={onChange}
value={value}
/>
)}
/>
<Controller
name="passwordConfirmation"
control={control}
render={({ field: { onChange, value } }) => (
<PasswordInput
id="passwordConfirmation"
invalid={!!errors?.passwordConfirmation}
invalidText={errors?.passwordConfirmation?.message}
labelText={t('confirmPassword', 'Confirm new password')}
onChange={onChange}
value={value}
/>
)}
/>
<Button className={styles.submitButton} disabled={isChangingPassword} type="submit">
{isChangingPassword ? (
<InlineLoading description={t('changingLanguage', 'Changing password') + '...'} />
) : (
<span>{t('change', 'Change Password')}</span>
)}
</Button>
</Form>
</Tile>
</div>
);
};

export default ChangePassword;
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
@use '@openmrs/esm-styleguide/src/vars' as *;

.submitButton {
margin-top: 1.5rem;
width: 18rem;

:global(.cds--inline-loading) {
min-height: 1rem;
}

:global(.cds--inline-loading__text) {
font-size: unset;
}
}

.alignCenter {
display: flex;
text-align: center;
Expand All @@ -13,3 +28,23 @@
@extend .alignCenter;
}
}

.container {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
margin-top: 2.5rem;
}

.changePasswordCard {
border-radius: 0;
border: 1px solid $ui-03;
background-color: $ui-02;
width: 23rem;
padding: 2.5rem;
position: relative;
min-height: fit-content;
}
20 changes: 2 additions & 18 deletions packages/apps/esm-login-app/src/login/login.component.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { type To, useLocation, useNavigate } from 'react-router-dom';
import { type TFunction, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import { Button, InlineLoading, InlineNotification, PasswordInput, TextInput, Tile } from '@carbon/react';
import {
ArrowLeftIcon,
ArrowRightIcon,
getCoreTranslation,
interpolateUrl,
navigate as openmrsNavigate,
refetchCurrentUser,
useConfig,
useConnectivity,
useSession,
} from '@openmrs/esm-framework';
import { type ConfigSchema } from '../config-schema';
import Logo from '../logo.component';
import styles from './login.scss';

export interface LoginReferrer {
Expand Down Expand Up @@ -280,20 +280,4 @@ const Login: React.FC = () => {
return null;
};

const Logo: React.FC<{ t: TFunction }> = ({ t }) => {
const { logo } = useConfig<ConfigSchema>();
return logo.src ? (
<img
alt={logo.alt ? t(logo.alt) : t('openmrsLogo', 'OpenMRS logo')}
className={styles.logoImg}
src={interpolateUrl(logo.src)}
/>
) : (
<svg role="img" className={styles.logo}>
<title>{t('openmrsLogo', 'OpenMRS logo')}</title>
<use xlinkHref="#omrs-logo-full-color"></use>
</svg>
);
};

export default Login;
23 changes: 23 additions & 0 deletions packages/apps/esm-login-app/src/logo.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { interpolateUrl, useConfig } from '@openmrs/esm-framework';
import { type TFunction } from 'react-i18next';
import { type ConfigSchema } from './config-schema';
import styles from './login/login.scss';

const Logo: React.FC<{ t: TFunction }> = ({ t }) => {
const { logo } = useConfig<ConfigSchema>();
return logo.src ? (
<img
alt={logo.alt ? t(logo.alt) : t('openmrsLogo', 'OpenMRS logo')}
className={styles.logoImg}
src={interpolateUrl(logo.src)}
/>
) : (
<svg role="img" className={styles.logo}>
<title>{t('openmrsLogo', 'OpenMRS logo')}</title>
<use xlinkHref="#omrs-logo-full-color"></use>
</svg>
);
};

export default Logo;
6 changes: 4 additions & 2 deletions packages/apps/esm-login-app/src/root.component.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import Login from './login/login.component';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import ChangePassword from './change-password/change-password.component';
import LocationPicker from './location-picker/location-picker.component';
import Login from './login/login.component';
import RedirectLogout from './redirect-logout/redirect-logout.component';
import { BrowserRouter, Route, Routes } from 'react-router-dom';

export interface RootProps {}

Expand All @@ -14,6 +15,7 @@ const Root: React.FC<RootProps> = () => {
<Route path="login/confirm" element={<Login />} />
<Route path="login/location" element={<LocationPicker />} />
<Route path="logout" element={<RedirectLogout />} />
<Route path="change-password" element={<ChangePassword />} />
</Routes>
</BrowserRouter>
);
Expand Down
6 changes: 6 additions & 0 deletions packages/apps/esm-login-app/src/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
"route": "logout",
"online": true,
"offline": true
},
{
"component": "root",
"route": "change-password",
"online": true,
"offline": true
}
],
"extensions": [
Expand Down
2 changes: 1 addition & 1 deletion packages/apps/esm-primary-navigation-app/src/routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"pages": [
{
"component": "root",
"routeRegex": "^(?!login/?)",
"routeRegex": "^(?!(?:login|change-password)/?)",
"online": true,
"offline": true,
"order": 0
Expand Down

0 comments on commit 6bad1ba

Please sign in to comment.