Skip to content

Commit a5851a1

Browse files
authored
(feat) O3-4100 - Login flow single-page redesign (#1192)
1 parent 34b4a42 commit a5851a1

File tree

3 files changed

+207
-147
lines changed

3 files changed

+207
-147
lines changed

packages/apps/esm-login-app/src/footer.component.tsx

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
2-
import { interpolateUrl, useConfig } from '@openmrs/esm-framework';
2+
import { useConfig, ArrowRightIcon } from '@openmrs/esm-framework';
3+
import { Tile, Button } from '@carbon/react';
34
import { useTranslation } from 'react-i18next';
45
import { type ConfigSchema } from './config-schema';
56
import styles from './login/login.scss';
@@ -11,11 +12,28 @@ const Footer: React.FC = () => {
1112

1213
return (
1314
<div className={styles.footer}>
14-
<p className={styles.poweredByTxt}>{t('poweredBy', 'Powered by')}</p>
15+
<Tile className={styles.poweredByTile}>
16+
<div className={styles.poweredByContainer}>
17+
<span className={styles.poweredByText}>{t('builtWith', 'Built with')}</span>
18+
<svg role="img" className={styles.poweredByLogo}>
19+
<use href="#omrs-logo-full-color"></use>
20+
</svg>
21+
<span className={styles.poweredByText}>
22+
{t('poweredBySubtext', 'An open-source medical record system and global community')}
23+
</span>
24+
<Button
25+
className={styles.learnMore}
26+
iconDescription={t('learnMore', 'Learn More')}
27+
kind="ghost"
28+
onClick={() => window.open('https://openmrs.org', '_blank')}
29+
renderIcon={(props) => <ArrowRightIcon {...props} size={20} className={styles.arrowRightIcon}/>}
30+
>
31+
<span>{t('learnMore', 'Learn More')}</span>
32+
</Button>
33+
</div>
34+
</Tile>
35+
1536
<div className={styles.logosContainer}>
16-
<svg role="img" className={styles.poweredByLogo}>
17-
<use href="#omrs-logo-partial-mono"></use>
18-
</svg>
1937
{logos.map((logo, index) => (
2038
<img
2139
key={index}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React, { useState, useRef, useEffect, useCallback } from 'react';
2-
import { type To, useLocation, useNavigate } from 'react-router-dom';
2+
import { useLocation, useNavigate } from 'react-router-dom';
33
import { useTranslation } from 'react-i18next';
44
import { Button, InlineLoading, InlineNotification, PasswordInput, TextInput, Tile } from '@carbon/react';
55
import {
6-
ArrowLeftIcon,
76
ArrowRightIcon,
87
getCoreTranslation,
9-
navigate as openmrsNavigate,
108
refetchCurrentUser,
9+
navigate as openmrsNavigate,
1110
useConfig,
1211
useConnectivity,
1312
useSession,
@@ -21,13 +20,6 @@ export interface LoginReferrer {
2120
referrer?: string;
2221
}
2322

24-
const hidden: React.CSSProperties = {
25-
height: 0,
26-
width: 0,
27-
border: 0,
28-
padding: 0,
29-
};
30-
3123
const Login: React.FC = () => {
3224
const { showPasswordOnSeparateScreen, provider: loginProvider, links: loginLinks } = useConfig<ConfigSchema>();
3325
const isLoginEnabled = useConnectivity();
@@ -36,26 +28,16 @@ const Login: React.FC = () => {
3628
const location = useLocation() as unknown as Omit<Location, 'state'> & {
3729
state: LoginReferrer;
3830
};
39-
40-
const rawNavigate = useNavigate();
41-
const navigate = useCallback(
42-
(to: To) => {
43-
rawNavigate(to, { state: location.state });
44-
},
45-
[rawNavigate, location.state],
46-
);
31+
const navigate = useNavigate();
4732

4833
const [errorMessage, setErrorMessage] = useState('');
4934
const [isLoggingIn, setIsLoggingIn] = useState(false);
5035
const [password, setPassword] = useState('');
5136
const [username, setUsername] = useState('');
52-
const formRef = useRef<HTMLFormElement>(null);
37+
const [showPasswordField, setShowPasswordField] = useState(false);
5338
const passwordInputRef = useRef<HTMLInputElement>(null);
5439
const usernameInputRef = useRef<HTMLInputElement>(null);
5540

56-
const showUsername = location.pathname === '/login';
57-
const showPassword = !showPasswordOnSeparateScreen || location.pathname === '/login/confirm';
58-
5941
useEffect(() => {
6042
if (!user) {
6143
if (loginProvider.type === 'oauth2') {
@@ -67,45 +49,49 @@ const Login: React.FC = () => {
6749
}, [username, navigate, location, user, loginProvider]);
6850

6951
useEffect(() => {
70-
const fieldToFocus =
71-
showPasswordOnSeparateScreen && showPassword ? passwordInputRef.current : usernameInputRef.current;
72-
73-
fieldToFocus?.focus();
74-
}, [showPassword, showPasswordOnSeparateScreen]);
52+
if (showPasswordOnSeparateScreen) {
53+
if (showPasswordField) {
54+
passwordInputRef.current?.focus();
55+
} else {
56+
usernameInputRef.current?.focus();
57+
}
58+
}
59+
}, [showPasswordField, showPasswordOnSeparateScreen]);
7560

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

79-
if (usernameField.value && usernameField.value.trim()) {
80-
navigate('/login/confirm');
64+
if (usernameField?.value.trim()) {
65+
setShowPasswordField(true);
8166
} else {
82-
usernameField.focus();
67+
usernameField?.focus();
8368
}
84-
}, [location.state, navigate]);
69+
}, []);
8570

8671
const changeUsername = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setUsername(evt.target.value), []);
87-
8872
const changePassword = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setPassword(evt.target.value), []);
8973

9074
const handleSubmit = useCallback(
9175
async (evt: React.FormEvent<HTMLFormElement>) => {
9276
evt.preventDefault();
9377
evt.stopPropagation();
9478

95-
if (!showPassword) {
79+
if (showPasswordOnSeparateScreen && !showPasswordField) {
9680
continueLogin();
9781
return false;
98-
} else if (!password || !password.trim()) {
99-
passwordInputRef.current.focus();
82+
}
83+
84+
if (!password || !password.trim()) {
85+
passwordInputRef.current?.focus();
10086
return false;
10187
}
10288

10389
try {
10490
setIsLoggingIn(true);
105-
10691
const sessionStore = await refetchCurrentUser(username, password);
10792
const session = sessionStore.session;
10893
const authenticated = sessionStore?.session?.authenticated;
94+
10995
if (authenticated) {
11096
if (session.sessionLocation) {
11197
let to = loginLinks?.loginSuccess || '/home';
@@ -125,9 +111,8 @@ const Login: React.FC = () => {
125111
setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
126112
setUsername('');
127113
setPassword('');
128-
129114
if (showPasswordOnSeparateScreen) {
130-
navigate('/login');
115+
setShowPasswordField(false);
131116
}
132117
}
133118

@@ -138,24 +123,19 @@ const Login: React.FC = () => {
138123
} else {
139124
setErrorMessage(t('invalidCredentials', 'Invalid username or password'));
140125
}
141-
142126
setUsername('');
143127
setPassword('');
144-
145128
if (showPasswordOnSeparateScreen) {
146-
navigate('/login');
129+
setShowPasswordField(false);
147130
}
148131
} finally {
149132
setIsLoggingIn(false);
150133
}
151-
152-
return false;
153134
},
154-
155-
[showPassword, username, password, navigate],
135+
[username, password, navigate, showPasswordOnSeparateScreen],
156136
);
157137

158-
if (!loginProvider || loginProvider.type === 'basic') {
138+
if (!loginProvider || loginProvider.type === 'basic'){
159139
return (
160140
<div className={styles.container}>
161141
<Tile className={styles.loginCard}>
@@ -169,109 +149,96 @@ const Login: React.FC = () => {
169149
/>
170150
</div>
171151
)}
172-
{showPasswordOnSeparateScreen && showPassword ? (
173-
<div className={styles.backButtonDiv}>
174-
<Button
175-
className={styles.backButton}
176-
iconDescription={t('backToUserNameIconLabel', 'Back to username')}
177-
kind="ghost"
178-
onClick={() => navigate('/login')}
179-
renderIcon={(props) => <ArrowLeftIcon {...props} size={24} />}
180-
>
181-
<span>{t('back', 'Back')}</span>
182-
</Button>
183-
</div>
184-
) : null}
185152
<div className={styles.center}>
186153
<Logo t={t} />
187154
</div>
188-
<form onSubmit={handleSubmit} ref={formRef}>
189-
{showUsername && (
190-
<div className={styles.inputGroup}>
191-
<TextInput
192-
id="username"
193-
type="text"
194-
name="username"
195-
labelText={t('username', 'Username')}
196-
value={username}
197-
onChange={changeUsername}
198-
ref={usernameInputRef}
199-
autoFocus
200-
required
201-
/>
202-
{/* For password managers */}
203-
{showPasswordOnSeparateScreen && (
204-
<input
205-
id="password"
206-
style={hidden}
207-
type="password"
208-
name="password"
209-
value={password}
210-
onChange={changePassword}
211-
/>
212-
)}
213-
{showPasswordOnSeparateScreen && (
155+
<form onSubmit={handleSubmit}>
156+
<div className={styles.inputGroup}>
157+
<TextInput
158+
id="username"
159+
type="text"
160+
labelText={t('username', 'Username')}
161+
value={username}
162+
onChange={changeUsername}
163+
ref={usernameInputRef}
164+
required
165+
autoFocus
166+
/>
167+
{showPasswordOnSeparateScreen ? (
168+
showPasswordField ? (
169+
<>
170+
<PasswordInput
171+
id="password"
172+
labelText={t('password', 'Password')}
173+
name="password"
174+
onChange={changePassword}
175+
ref={passwordInputRef}
176+
required
177+
value={password}
178+
showPasswordLabel={t('showPassword', 'Show password')}
179+
invalidText={t('validValueRequired', 'A valid value is required')}
180+
/>
181+
<Button
182+
type="submit"
183+
className={styles.continueButton}
184+
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
185+
iconDescription="Log in"
186+
disabled={!isLoginEnabled || isLoggingIn}
187+
>
188+
{isLoggingIn ? (
189+
<InlineLoading className={styles.loader} description={t('loggingIn', 'Logging in') + '...'} />
190+
) : (
191+
t('login', 'Log in')
192+
)}
193+
</Button>
194+
</>
195+
) : (
214196
<Button
215197
className={styles.continueButton}
216198
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
217-
type="submit"
218-
iconDescription="Continue to login"
199+
iconDescription="Continue to password"
219200
onClick={continueLogin}
220201
disabled={!isLoginEnabled}
221202
>
222203
{t('continue', 'Continue')}
223204
</Button>
224-
)}
225-
</div>
226-
)}
227-
{showPassword && (
228-
<div className={styles.inputGroup}>
229-
<PasswordInput
230-
id="password"
231-
invalidText={t('validValueRequired', 'A valid value is required')}
232-
labelText={t('password', 'Password')}
233-
name="password"
234-
onChange={changePassword}
235-
ref={passwordInputRef}
236-
required
237-
showPasswordLabel={t('showPassword', 'Show password')}
238-
value={password}
239-
/>
240-
{/* For password managers */}
241-
{showPasswordOnSeparateScreen && (
242-
<input
243-
id="username"
244-
type="text"
245-
name="username"
246-
style={hidden}
247-
value={username}
248-
onChange={changeUsername}
205+
)
206+
) : (
207+
<>
208+
<PasswordInput
209+
id="password"
210+
labelText={t('password', 'Password')}
211+
name="password"
212+
onChange={changePassword}
213+
ref={passwordInputRef}
249214
required
215+
value={password}
216+
showPasswordLabel={t('showPassword', 'Show password')}
217+
invalidText={t('validValueRequired', 'A valid value is required')}
250218
/>
251-
)}
252-
<Button
253-
type="submit"
254-
className={styles.continueButton}
255-
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
256-
iconDescription="Log in"
257-
disabled={!isLoginEnabled || isLoggingIn}
258-
>
259-
{isLoggingIn ? (
260-
<InlineLoading className={styles.loader} description={t('loggingIn', 'Logging in') + '...'} />
261-
) : (
262-
<span>{t('login', 'Log in')}</span>
263-
)}
264-
</Button>
265-
</div>
266-
)}
219+
<Button
220+
type="submit"
221+
className={styles.continueButton}
222+
renderIcon={(props) => <ArrowRightIcon size={24} {...props} />}
223+
iconDescription="Log in"
224+
disabled={!isLoginEnabled || isLoggingIn}
225+
>
226+
{isLoggingIn ? (
227+
<InlineLoading className={styles.loader} description={t('loggingIn', 'Logging in') + '...'} />
228+
) : (
229+
t('login', 'Log in')
230+
)}
231+
</Button>
232+
</>
233+
)}
234+
</div>
267235
</form>
268236
</Tile>
269-
<Footer/>
237+
<Footer />
270238
</div>
271239
);
272240
}
273-
274-
return null;
241+
return null;
275242
};
276243

277244
export default Login;

0 commit comments

Comments
 (0)