Skip to content

Commit

Permalink
(feat) O3-4100 - Login flow single-page redesign (#1192)
Browse files Browse the repository at this point in the history
  • Loading branch information
suubi-joshua authored Dec 15, 2024
1 parent 34b4a42 commit a5851a1
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 147 deletions.
28 changes: 23 additions & 5 deletions packages/apps/esm-login-app/src/footer.component.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { interpolateUrl, useConfig } from '@openmrs/esm-framework';
import { useConfig, ArrowRightIcon } from '@openmrs/esm-framework';
import { Tile, Button } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { type ConfigSchema } from './config-schema';
import styles from './login/login.scss';
Expand All @@ -11,11 +12,28 @@ const Footer: React.FC = () => {

return (
<div className={styles.footer}>
<p className={styles.poweredByTxt}>{t('poweredBy', 'Powered by')}</p>
<Tile className={styles.poweredByTile}>
<div className={styles.poweredByContainer}>
<span className={styles.poweredByText}>{t('builtWith', 'Built with')}</span>
<svg role="img" className={styles.poweredByLogo}>
<use href="#omrs-logo-full-color"></use>
</svg>
<span className={styles.poweredByText}>
{t('poweredBySubtext', 'An open-source medical record system and global community')}
</span>
<Button
className={styles.learnMore}
iconDescription={t('learnMore', 'Learn More')}
kind="ghost"
onClick={() => window.open('https://openmrs.org', '_blank')}
renderIcon={(props) => <ArrowRightIcon {...props} size={20} className={styles.arrowRightIcon}/>}
>
<span>{t('learnMore', 'Learn More')}</span>
</Button>
</div>
</Tile>

<div className={styles.logosContainer}>
<svg role="img" className={styles.poweredByLogo}>
<use href="#omrs-logo-partial-mono"></use>
</svg>
{logos.map((logo, index) => (
<img
key={index}
Expand Down
229 changes: 98 additions & 131 deletions packages/apps/esm-login-app/src/login/login.component.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { type To, useLocation, useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button, InlineLoading, InlineNotification, PasswordInput, TextInput, Tile } from '@carbon/react';
import {
ArrowLeftIcon,
ArrowRightIcon,
getCoreTranslation,
navigate as openmrsNavigate,
refetchCurrentUser,
navigate as openmrsNavigate,
useConfig,
useConnectivity,
useSession,
Expand All @@ -21,13 +20,6 @@ export interface LoginReferrer {
referrer?: string;
}

const hidden: React.CSSProperties = {
height: 0,
width: 0,
border: 0,
padding: 0,
};

const Login: React.FC = () => {
const { showPasswordOnSeparateScreen, provider: loginProvider, links: loginLinks } = useConfig<ConfigSchema>();
const isLoginEnabled = useConnectivity();
Expand All @@ -36,26 +28,16 @@ const Login: React.FC = () => {
const location = useLocation() as unknown as Omit<Location, 'state'> & {
state: LoginReferrer;
};

const rawNavigate = useNavigate();
const navigate = useCallback(
(to: To) => {
rawNavigate(to, { state: location.state });
},
[rawNavigate, location.state],
);
const navigate = useNavigate();

const [errorMessage, setErrorMessage] = useState('');
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [password, setPassword] = useState('');
const [username, setUsername] = useState('');
const formRef = useRef<HTMLFormElement>(null);
const [showPasswordField, setShowPasswordField] = useState(false);
const passwordInputRef = useRef<HTMLInputElement>(null);
const usernameInputRef = useRef<HTMLInputElement>(null);

const showUsername = location.pathname === '/login';
const showPassword = !showPasswordOnSeparateScreen || location.pathname === '/login/confirm';

useEffect(() => {
if (!user) {
if (loginProvider.type === 'oauth2') {
Expand All @@ -67,45 +49,49 @@ const Login: React.FC = () => {
}, [username, navigate, location, user, loginProvider]);

useEffect(() => {
const fieldToFocus =
showPasswordOnSeparateScreen && showPassword ? passwordInputRef.current : usernameInputRef.current;

fieldToFocus?.focus();
}, [showPassword, showPasswordOnSeparateScreen]);
if (showPasswordOnSeparateScreen) {
if (showPasswordField) {
passwordInputRef.current?.focus();
} else {
usernameInputRef.current?.focus();
}
}
}, [showPasswordField, showPasswordOnSeparateScreen]);

const continueLogin = useCallback(() => {
const usernameField = usernameInputRef.current;

if (usernameField.value && usernameField.value.trim()) {
navigate('/login/confirm');
if (usernameField?.value.trim()) {
setShowPasswordField(true);
} else {
usernameField.focus();
usernameField?.focus();
}
}, [location.state, navigate]);
}, []);

const changeUsername = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setUsername(evt.target.value), []);

const changePassword = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setPassword(evt.target.value), []);

const handleSubmit = useCallback(
async (evt: React.FormEvent<HTMLFormElement>) => {
evt.preventDefault();
evt.stopPropagation();

if (!showPassword) {
if (showPasswordOnSeparateScreen && !showPasswordField) {
continueLogin();
return false;
} else if (!password || !password.trim()) {
passwordInputRef.current.focus();
}

if (!password || !password.trim()) {
passwordInputRef.current?.focus();
return false;
}

try {
setIsLoggingIn(true);

const sessionStore = await refetchCurrentUser(username, password);
const session = sessionStore.session;
const authenticated = sessionStore?.session?.authenticated;

if (authenticated) {
if (session.sessionLocation) {
let to = loginLinks?.loginSuccess || '/home';
Expand All @@ -125,9 +111,8 @@ const Login: React.FC = () => {
setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
setUsername('');
setPassword('');

if (showPasswordOnSeparateScreen) {
navigate('/login');
setShowPasswordField(false);
}
}

Expand All @@ -138,24 +123,19 @@ const Login: React.FC = () => {
} else {
setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
}

setUsername('');
setPassword('');

if (showPasswordOnSeparateScreen) {
navigate('/login');
setShowPasswordField(false);
}
} finally {
setIsLoggingIn(false);
}

return false;
},

[showPassword, username, password, navigate],
[username, password, navigate, showPasswordOnSeparateScreen],
);

if (!loginProvider || loginProvider.type === 'basic') {
if (!loginProvider || loginProvider.type === 'basic'){
return (
<div className={styles.container}>
<Tile className={styles.loginCard}>
Expand All @@ -169,109 +149,96 @@ const Login: React.FC = () => {
/>
</div>
)}
{showPasswordOnSeparateScreen && showPassword ? (
<div className={styles.backButtonDiv}>
<Button
className={styles.backButton}
iconDescription={t('backToUserNameIconLabel', 'Back to username')}
kind="ghost"
onClick={() => navigate('/login')}
renderIcon={(props) => <ArrowLeftIcon {...props} size={24} />}
>
<span>{t('back', 'Back')}</span>
</Button>
</div>
) : null}
<div className={styles.center}>
<Logo t={t} />
</div>
<form onSubmit={handleSubmit} ref={formRef}>
{showUsername && (
<div className={styles.inputGroup}>
<TextInput
id="username"
type="text"
name="username"
labelText={t('username', 'Username')}
value={username}
onChange={changeUsername}
ref={usernameInputRef}
autoFocus
required
/>
{/* For password managers */}
{showPasswordOnSeparateScreen && (
<input
id="password"
style={hidden}
type="password"
name="password"
value={password}
onChange={changePassword}
/>
)}
{showPasswordOnSeparateScreen && (
<form onSubmit={handleSubmit}>
<div className={styles.inputGroup}>
<TextInput
id="username"
type="text"
labelText={t('username', 'Username')}
value={username}
onChange={changeUsername}
ref={usernameInputRef}
required
autoFocus
/>
{showPasswordOnSeparateScreen ? (
showPasswordField ? (
<>
<PasswordInput
id="password"
labelText={t('password', 'Password')}
name="password"
onChange={changePassword}
ref={passwordInputRef}
required
value={password}
showPasswordLabel={t('showPassword', 'Show password')}
invalidText={t('validValueRequired', 'A valid value is required')}
/>
<Button
type="submit"
className={styles.continueButton}
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
iconDescription="Log in"
disabled={!isLoginEnabled || isLoggingIn}
>
{isLoggingIn ? (
<InlineLoading className={styles.loader} description={t('loggingIn', 'Logging in') + '...'} />
) : (
t('login', 'Log in')
)}
</Button>
</>
) : (
<Button
className={styles.continueButton}
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
type="submit"
iconDescription="Continue to login"
iconDescription="Continue to password"
onClick={continueLogin}
disabled={!isLoginEnabled}
>
{t('continue', 'Continue')}
</Button>
)}
</div>
)}
{showPassword && (
<div className={styles.inputGroup}>
<PasswordInput
id="password"
invalidText={t('validValueRequired', 'A valid value is required')}
labelText={t('password', 'Password')}
name="password"
onChange={changePassword}
ref={passwordInputRef}
required
showPasswordLabel={t('showPassword', 'Show password')}
value={password}
/>
{/* For password managers */}
{showPasswordOnSeparateScreen && (
<input
id="username"
type="text"
name="username"
style={hidden}
value={username}
onChange={changeUsername}
)
) : (
<>
<PasswordInput
id="password"
labelText={t('password', 'Password')}
name="password"
onChange={changePassword}
ref={passwordInputRef}
required
value={password}
showPasswordLabel={t('showPassword', 'Show password')}
invalidText={t('validValueRequired', 'A valid value is required')}
/>
)}
<Button
type="submit"
className={styles.continueButton}
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
iconDescription="Log in"
disabled={!isLoginEnabled || isLoggingIn}
>
{isLoggingIn ? (
<InlineLoading className={styles.loader} description={t('loggingIn', 'Logging in') + '...'} />
) : (
<span>{t('login', 'Log in')}</span>
)}
</Button>
</div>
)}
<Button
type="submit"
className={styles.continueButton}
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
iconDescription="Log in"
disabled={!isLoginEnabled || isLoggingIn}
>
{isLoggingIn ? (
<InlineLoading className={styles.loader} description={t('loggingIn', 'Logging in') + '...'} />
) : (
t('login', 'Log in')
)}
</Button>
</>
)}
</div>
</form>
</Tile>
<Footer/>
<Footer />
</div>
);
}

return null;
return null;
};

export default Login;
Loading

0 comments on commit a5851a1

Please sign in to comment.