0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

refactor(ui): update set password form (#3020)

This commit is contained in:
simeng-li 2023-02-06 17:02:07 +08:00 committed by GitHub
parent 4a407ad4de
commit 4a84162722
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 336 additions and 73 deletions

View file

@ -35,9 +35,9 @@ describe('smoke testing', () => {
expect(page.url()).toBe(new URL('register/username/password', logtoUrl).href); expect(page.url()).toBe(new URL('register/username/password', logtoUrl).href);
const passwordField = await page.waitForSelector('input[name=new-password]'); const passwordField = await page.waitForSelector('input[name=newPassword]');
const confirmPasswordField = await page.waitForSelector('input[name=confirm-new-password]'); const confirmPasswordField = await page.waitForSelector('input[name=confirmPassword]');
const saveButton = await page.waitForSelector('button'); const saveButton = await page.waitForSelector('button[name=submit]');
await passwordField.type(consolePassword); await passwordField.type(consolePassword);
await confirmPasswordField.type(consolePassword); await confirmPasswordField.type(consolePassword);

View file

@ -38,6 +38,7 @@ const translation = {
link_another_email: 'Link another email', // UNTRANSLATED link_another_email: 'Link another email', // UNTRANSLATED
link_another_phone: 'Link another phone', // UNTRANSLATED link_another_phone: 'Link another phone', // UNTRANSLATED
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
show_password: 'Show password', // UNTRANSLATED
}, },
description: { description: {
email: 'Email', email: 'Email',

View file

@ -36,6 +36,7 @@ const translation = {
link_another_email: 'Link another email', link_another_email: 'Link another email',
link_another_phone: 'Link another phone', link_another_phone: 'Link another phone',
link_another_email_or_phone: 'Link another email or phone', link_another_email_or_phone: 'Link another email or phone',
show_password: 'Show password',
}, },
description: { description: {
email: 'email', email: 'email',

View file

@ -38,6 +38,7 @@ const translation = {
link_another_email: 'Link another email', // UNTRANSLATED link_another_email: 'Link another email', // UNTRANSLATED
link_another_phone: 'Link another phone', // UNTRANSLATED link_another_phone: 'Link another phone', // UNTRANSLATED
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
show_password: 'Show password', // UNTRANSLATED
}, },
description: { description: {
email: 'email', email: 'email',

View file

@ -38,6 +38,7 @@ const translation = {
link_another_email: 'Link another email', // UNTRANSLATED link_another_email: 'Link another email', // UNTRANSLATED
link_another_phone: 'Link another phone', // UNTRANSLATED link_another_phone: 'Link another phone', // UNTRANSLATED
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
show_password: 'Show password', // UNTRANSLATED
}, },
description: { description: {
email: '이메일', email: '이메일',

View file

@ -38,6 +38,7 @@ const translation = {
link_another_email: 'Link another email', // UNTRANSLATED link_another_email: 'Link another email', // UNTRANSLATED
link_another_phone: 'Link another phone', // UNTRANSLATED link_another_phone: 'Link another phone', // UNTRANSLATED
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
show_password: 'Show password', // UNTRANSLATED
}, },
description: { description: {
email: 'e-mail', email: 'e-mail',

View file

@ -38,6 +38,7 @@ const translation = {
link_another_email: 'Link another email', // UNTRANSLATED link_another_email: 'Link another email', // UNTRANSLATED
link_another_phone: 'Link another phone', // UNTRANSLATED link_another_phone: 'Link another phone', // UNTRANSLATED
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
show_password: 'Show password', // UNTRANSLATED
}, },
description: { description: {
email: 'email', email: 'email',

View file

@ -38,6 +38,7 @@ const translation = {
link_another_email: 'Link another email', // UNTRANSLATED link_another_email: 'Link another email', // UNTRANSLATED
link_another_phone: 'Link another phone', // UNTRANSLATED link_another_phone: 'Link another phone', // UNTRANSLATED
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
show_password: 'Show password', // UNTRANSLATED
}, },
description: { description: {
email: 'e-posta adresi', email: 'e-posta adresi',

View file

@ -38,6 +38,7 @@ const translation = {
link_another_email: 'Link another email', // UNTRANSLATED link_another_email: 'Link another email', // UNTRANSLATED
link_another_phone: 'Link another phone', // UNTRANSLATED link_another_phone: 'Link another phone', // UNTRANSLATED
link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED link_another_email_or_phone: 'Link another email or phone', // UNTRANSLATED
show_password: 'Show password', // UNTRANSLATED
}, },
description: { description: {
email: '邮箱', email: '邮箱',

View file

@ -61,6 +61,7 @@
"react": "^18.0.0", "react": "^18.0.0",
"react-device-detect": "^2.2.2", "react-device-detect": "^2.2.2",
"react-dom": "^18.0.0", "react-dom": "^18.0.0",
"react-hook-form": "^7.34.0",
"react-i18next": "^11.18.3", "react-i18next": "^11.18.3",
"react-modal": "^3.15.1", "react-modal": "^3.15.1",
"react-router-dom": "^6.2.2", "react-router-dom": "^6.2.2",

View file

@ -0,0 +1,90 @@
@use '@/scss/underscore' as _;
.inputField {
position: relative;
@include _.flex-row;
border: _.border(var(--color-line-border));
border-radius: var(--radius);
overflow: hidden;
transition-property: outline, border;
transition-timing-function: ease-in-out;
transition-duration: 0.2s;
// fix in safari input field line-height issue
height: 44px;
input {
padding: 0 _.unit(4);
flex: 1;
background: none;
caret-color: var(--color-brand-default);
font: var(--font-body-1);
color: var(--color-type-primary);
align-self: stretch;
&::placeholder {
color: var(--color-type-secondary);
}
&:not(:last-child) {
padding-right: _.unit(10);
}
}
.suffix {
position: absolute;
right: _.unit(4);
top: 50%;
transform: translateY(-50%);
width: _.unit(8);
height: _.unit(8);
display: none;
&.visible {
display: flex;
}
}
&:focus-within {
border: _.border(var(--color-brand-default));
input {
outline: none;
}
.focusVisible {
display: flex;
}
}
&.danger {
border: _.border(var(--color-danger-default));
input {
caret-color: var(--color-danger-default);
}
}
}
.errorMessage {
margin-top: _.unit(1);
margin-left: _.unit(0.5);
}
:global(body.desktop) {
.inputField {
outline: 3px solid transparent;
input {
font: var(--font-body-2);
}
&:focus-within {
outline-color: var(--color-overlay-brand-focused);
}
&.danger:focus-within {
outline-color: var(--color-overlay-danger-focused);
}
}
}

View file

@ -0,0 +1,40 @@
import classNames from 'classnames';
import type { ForwardedRef, HTMLProps, ReactElement } from 'react';
import { forwardRef, cloneElement } from 'react';
import type { ErrorType } from '@/components/ErrorMessage';
import ErrorMessage from '@/components/ErrorMessage';
import * as styles from './index.module.scss';
type Props = HTMLProps<HTMLInputElement> & {
className?: string;
error?: ErrorType;
isDanger?: boolean;
suffix?: ReactElement;
isSuffixFocusVisible?: boolean;
isSuffixVisible?: boolean;
};
const InputField = (
{ className, error, isDanger, suffix, isSuffixFocusVisible, isSuffixVisible, ...props }: Props,
reference: ForwardedRef<HTMLInputElement>
) => (
<div className={className}>
<div className={classNames(styles.inputField, isDanger && styles.danger)}>
<input {...props} ref={reference} />
{suffix &&
cloneElement(suffix, {
className: classNames([
suffix.props.className,
styles.suffix,
isSuffixFocusVisible && styles.focusVisible,
isSuffixVisible && styles.visible,
]),
})}
</div>
{error && <ErrorMessage error={error} className={styles.errorMessage} />}
</div>
);
export default forwardRef(InputField);

View file

@ -7,7 +7,7 @@
cursor: pointer; cursor: pointer;
} }
.checkBox { .checkbox {
margin-right: _.unit(2); margin-right: _.unit(2);
fill: var(--color-type-secondary); fill: var(--color-type-secondary);
cursor: pointer; cursor: pointer;

View file

@ -40,7 +40,7 @@ const TermsOfUse = ({ name, className, termsUrl, isChecked, onChange, onTermsCli
' ': toggle, ' ': toggle,
})} })}
> >
<Checkbox name={name} checked={isChecked} className={styles.checkBox} /> <Checkbox name={name} checked={isChecked} className={styles.checkbox} />
<div className={styles.content}> <div className={styles.content}>
{prefix} {prefix}
<TextLink <TextLink

View file

@ -0,0 +1,41 @@
import { useTranslation } from 'react-i18next';
import Checkbox from '@/components/Checkbox';
import { onKeyDownHandler } from '@/utils/a11y';
import * as styles from './index.module.scss';
type Props = {
isChecked?: boolean;
onChange: (checked: boolean) => void;
};
const TogglePassword = ({ isChecked, onChange }: Props) => {
const { t } = useTranslation();
const toggle = () => {
onChange(!isChecked);
};
return (
<div
role="radio"
aria-checked={isChecked}
tabIndex={0}
className={styles.passwordSwitch}
onClick={toggle}
onKeyDown={onKeyDownHandler({
Escape: () => {
onChange(false);
},
Enter: toggle,
' ': toggle,
})}
>
<Checkbox name="toggle-password" checked={isChecked} className={styles.checkbox} />
<div>{t('action.show_password')}</div>
</div>
);
};
export default TogglePassword;

View file

@ -8,7 +8,8 @@
} }
.inputField, .inputField,
.formErrors { .formErrors,
.passwordSwitch {
margin-bottom: _.unit(4); margin-bottom: _.unit(4);
} }
@ -17,3 +18,16 @@
margin-top: _.unit(-3); margin-top: _.unit(-3);
} }
} }
.passwordSwitch {
@include _.flex-row;
width: 100%;
user-select: none;
cursor: pointer;
.checkbox {
margin-right: _.unit(2);
fill: var(--color-type-secondary);
cursor: pointer;
}
}

View file

@ -14,28 +14,36 @@ describe('<SetPassword />', () => {
const { queryByText, container } = render( const { queryByText, container } = render(
<SetPassword errorMessage="error" onSubmit={submit} /> <SetPassword errorMessage="error" onSubmit={submit} />
); );
expect(container.querySelector('input[name="new-password"]')).not.toBeNull(); expect(container.querySelector('input[name="newPassword"]')).not.toBeNull();
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull(); expect(container.querySelector('input[name="confirmPassword"]')).not.toBeNull();
expect(queryByText('error')).not.toBeNull(); expect(queryByText('error')).not.toBeNull();
expect(queryByText('action.save_password')).not.toBeNull(); expect(queryByText('action.save_password')).not.toBeNull();
}); });
test('password are required', () => { test('password is required', async () => {
const { queryByText, getByText } = render( const { queryByText, getByText } = render(
<SetPassword clearErrorMessage={clearError} onSubmit={submit} /> <SetPassword clearErrorMessage={clearError} onSubmit={submit} />
); );
const submitButton = getByText('action.save_password'); const submitButton = getByText('action.save_password');
fireEvent.click(submitButton);
act(() => {
fireEvent.click(submitButton);
});
expect(clearError).toBeCalled(); expect(clearError).toBeCalled();
expect(queryByText('password_required')).not.toBeNull();
await waitFor(() => {
expect(queryByText('password_required')).not.toBeNull();
});
expect(submit).not.toBeCalled(); expect(submit).not.toBeCalled();
}); });
test('password less than 6 chars should throw', () => { test('password less than 6 chars should throw', async () => {
const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />); const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />);
const submitButton = getByText('action.save_password'); const submitButton = getByText('action.save_password');
const passwordInput = container.querySelector('input[name="new-password"]'); const passwordInput = container.querySelector('input[name="newPassword"]');
if (passwordInput) { if (passwordInput) {
fireEvent.change(passwordInput, { target: { value: '12345' } }); fireEvent.change(passwordInput, { target: { value: '12345' } });
@ -45,7 +53,9 @@ describe('<SetPassword />', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
}); });
expect(queryByText('password_min_length')).not.toBeNull(); await waitFor(() => {
expect(queryByText('password_min_length')).not.toBeNull();
});
expect(submit).not.toBeCalled(); expect(submit).not.toBeCalled();
@ -56,14 +66,16 @@ describe('<SetPassword />', () => {
} }
}); });
expect(queryByText('password_min_length')).toBeNull(); await waitFor(() => {
expect(queryByText('password_min_length')).toBeNull();
});
}); });
test('password mismatch with confirmPassword should throw', () => { test('password mismatch with confirmPassword should throw', async () => {
const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />); const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />);
const submitButton = getByText('action.save_password'); const submitButton = getByText('action.save_password');
const passwordInput = container.querySelector('input[name="new-password"]'); const passwordInput = container.querySelector('input[name="newPassword"]');
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
act(() => { act(() => {
if (passwordInput) { if (passwordInput) {
@ -77,7 +89,9 @@ describe('<SetPassword />', () => {
fireEvent.click(submitButton); fireEvent.click(submitButton);
}); });
expect(queryByText('passwords_do_not_match')).not.toBeNull(); await waitFor(() => {
expect(queryByText('passwords_do_not_match')).not.toBeNull();
});
expect(submit).not.toBeCalled(); expect(submit).not.toBeCalled();
@ -88,14 +102,16 @@ describe('<SetPassword />', () => {
} }
}); });
expect(queryByText('passwords_do_not_match')).toBeNull(); await waitFor(() => {
expect(queryByText('passwords_do_not_match')).toBeNull();
});
}); });
test('should submit properly', async () => { test('should submit properly', async () => {
const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />); const { queryByText, getByText, container } = render(<SetPassword onSubmit={submit} />);
const submitButton = getByText('action.save_password'); const submitButton = getByText('action.save_password');
const passwordInput = container.querySelector('input[name="new-password"]'); const passwordInput = container.querySelector('input[name="newPassword"]');
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
act(() => { act(() => {
if (passwordInput) { if (passwordInput) {

View file

@ -1,13 +1,16 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback } from 'react'; import { useCallback, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import ClearIcon from '@/assets/icons/clear-icon.svg';
import Button from '@/components/Button'; import Button from '@/components/Button';
import IconButton from '@/components/Button/IconButton';
import ErrorMessage from '@/components/ErrorMessage'; import ErrorMessage from '@/components/ErrorMessage';
import Input from '@/components/Input'; import InputField from '@/components/InputField';
import useForm from '@/hooks/use-form'; import { passwordErrorWatcher } from '@/utils/form';
import { passwordValidation, confirmPasswordValidation } from '@/utils/field-validations';
import TogglePassword from './TogglePassword';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
type Props = { type Props = {
@ -20,15 +23,10 @@ type Props = {
}; };
type FieldState = { type FieldState = {
password: string; newPassword: string;
confirmPassword: string; confirmPassword: string;
}; };
const defaultState: FieldState = {
password: '',
confirmPassword: '',
};
const SetPassword = ({ const SetPassword = ({
className, className,
autoFocus, autoFocus,
@ -37,53 +35,93 @@ const SetPassword = ({
clearErrorMessage, clearErrorMessage,
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState);
const [showPassword, setShowPassword] = useState(false);
const {
register,
watch,
resetField,
handleSubmit,
formState: { errors },
} = useForm<FieldState>({
reValidateMode: 'onChange',
defaultValues: { newPassword: '', confirmPassword: '' },
});
const onSubmitHandler = useCallback( const onSubmitHandler = useCallback(
async (event?: React.FormEvent<HTMLFormElement>) => { (event?: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault();
clearErrorMessage?.(); clearErrorMessage?.();
if (!validateForm()) { void handleSubmit((data, event) => {
return; onSubmit(data.newPassword);
} event?.preventDefault();
})(event);
onSubmit(fieldValue.password);
}, },
[clearErrorMessage, validateForm, onSubmit, fieldValue.password] [clearErrorMessage, handleSubmit, onSubmit]
); );
const newPasswordError = passwordErrorWatcher(errors.newPassword);
return ( return (
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}> <form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
<Input <InputField
required
className={styles.inputField} className={styles.inputField}
name="new-password" type={showPassword ? 'text' : 'password'}
type="password"
autoComplete="new-password" autoComplete="new-password"
placeholder={t('input.password')} placeholder={t('input.password')}
autoFocus={autoFocus} autoFocus={autoFocus}
{...register('password', passwordValidation)} isDanger={!!newPasswordError}
onClear={() => { error={newPasswordError}
setFieldValue((state) => ({ ...state, password: '' })); aria-invalid={!!newPasswordError}
}} {...register('newPassword', { required: true, minLength: 6 })}
isSuffixFocusVisible={!!watch('newPassword')}
suffix={
<IconButton
onClick={() => {
resetField('newPassword');
}}
>
<ClearIcon />
</IconButton>
}
/> />
<Input
<InputField
required
className={styles.inputField} className={styles.inputField}
name="confirm-new-password" type={showPassword ? 'text' : 'password'}
type="password" autoComplete="new-password"
placeholder={t('input.confirm_password')} placeholder={t('input.confirm_password')}
{...register('confirmPassword', (confirmPassword) => error={errors.confirmPassword && 'passwords_do_not_match'}
confirmPasswordValidation(fieldValue.password, confirmPassword) aria-invalid={!!errors.confirmPassword}
)} {...register('confirmPassword', {
isErrorStyling={false} validate: (value) => value === watch('newPassword'),
onClear={() => { })}
setFieldValue((state) => ({ ...state, confirmPassword: '' })); isSuffixFocusVisible={!!watch('confirmPassword')}
}} suffix={
<IconButton
onClick={() => {
resetField('confirmPassword');
}}
>
<ClearIcon />
</IconButton>
}
/> />
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>} {errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
<Button title="action.save_password" onClick={async () => onSubmitHandler()} /> <TogglePassword isChecked={showPassword} onChange={setShowPassword} />
<Button
name="submit"
title="action.save_password"
onClick={async () => {
onSubmitHandler();
}}
/>
<input hidden type="submit" /> <input hidden type="submit" />
</form> </form>

View file

@ -24,8 +24,8 @@ describe('SetPassword', () => {
<SetPassword /> <SetPassword />
</SettingsProvider> </SettingsProvider>
); );
expect(container.querySelector('input[name="new-password"]')).not.toBeNull(); expect(container.querySelector('input[name="newPassword"]')).not.toBeNull();
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull(); expect(container.querySelector('input[name="confirmPassword"]')).not.toBeNull();
expect(queryByText('action.save_password')).not.toBeNull(); expect(queryByText('action.save_password')).not.toBeNull();
}); });
@ -36,8 +36,8 @@ describe('SetPassword', () => {
</SettingsProvider> </SettingsProvider>
); );
const submitButton = getByText('action.save_password'); const submitButton = getByText('action.save_password');
const passwordInput = container.querySelector('input[name="new-password"]'); const passwordInput = container.querySelector('input[name="newPassword"]');
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
act(() => { act(() => {
if (passwordInput) { if (passwordInput) {

View file

@ -36,8 +36,8 @@ describe('<PasswordRegisterWithUsername />', () => {
</SettingsProvider> </SettingsProvider>
); );
expect(container.querySelector('input[name="new-password"]')).not.toBeNull(); expect(container.querySelector('input[name="newPassword"]')).not.toBeNull();
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull(); expect(container.querySelector('input[name="confirmPassword"]')).not.toBeNull();
expect(queryByText('action.save_password')).not.toBeNull(); expect(queryByText('action.save_password')).not.toBeNull();
}); });
@ -56,7 +56,7 @@ describe('<PasswordRegisterWithUsername />', () => {
</SettingsProvider> </SettingsProvider>
); );
expect(container.querySelector('input[name="new-password"]')).toBeNull(); expect(container.querySelector('input[name="newPassword"]')).toBeNull();
expect(queryByText('description.not_found')).not.toBeNull(); expect(queryByText('description.not_found')).not.toBeNull();
}); });
@ -68,8 +68,8 @@ describe('<PasswordRegisterWithUsername />', () => {
); );
const submitButton = getByText('action.save_password'); const submitButton = getByText('action.save_password');
const passwordInput = container.querySelector('input[name="new-password"]'); const passwordInput = container.querySelector('input[name="newPassword"]');
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
act(() => { act(() => {
if (passwordInput) { if (passwordInput) {

View file

@ -27,16 +27,16 @@ describe('ForgotPassword', () => {
</MemoryRouter> </MemoryRouter>
); );
expect(container.querySelector('input[name="new-password"]')).not.toBeNull(); expect(container.querySelector('input[name="newPassword"]')).not.toBeNull();
expect(container.querySelector('input[name="confirm-new-password"]')).not.toBeNull(); expect(container.querySelector('input[name="confirmPassword"]')).not.toBeNull();
expect(queryByText('action.save_password')).not.toBeNull(); expect(queryByText('action.save_password')).not.toBeNull();
}); });
test('should submit properly', async () => { test('should submit properly', async () => {
const { getByText, container } = renderWithPageContext(<ResetPassword />); const { getByText, container } = renderWithPageContext(<ResetPassword />);
const submitButton = getByText('action.save_password'); const submitButton = getByText('action.save_password');
const passwordInput = container.querySelector('input[name="new-password"]'); const passwordInput = container.querySelector('input[name="newPassword"]');
const confirmPasswordInput = container.querySelector('input[name="confirm-new-password"]'); const confirmPasswordInput = container.querySelector('input[name="confirmPassword"]');
act(() => { act(() => {
if (passwordInput) { if (passwordInput) {

View file

@ -0,0 +1,13 @@
import type { FieldError } from 'react-hook-form';
import type { ErrorType } from '@/components/ErrorMessage';
export const passwordErrorWatcher = (error?: FieldError): ErrorType | undefined => {
switch (error?.type) {
case 'required':
return 'password_required';
case 'minLength':
return { code: 'password_min_length', data: { min: 6 } };
default:
}
};

View file

@ -813,6 +813,7 @@ importers:
react: ^18.0.0 react: ^18.0.0
react-device-detect: ^2.2.2 react-device-detect: ^2.2.2
react-dom: ^18.0.0 react-dom: ^18.0.0
react-hook-form: ^7.34.0
react-i18next: ^11.18.3 react-i18next: ^11.18.3
react-modal: ^3.15.1 react-modal: ^3.15.1
react-router-dom: ^6.2.2 react-router-dom: ^6.2.2
@ -868,6 +869,7 @@ importers:
react: 18.2.0 react: 18.2.0
react-device-detect: 2.2.2_biqbaboplfbrettd7655fr4n2y react-device-detect: 2.2.2_biqbaboplfbrettd7655fr4n2y
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
react-hook-form: 7.34.0_react@18.2.0
react-i18next: 11.18.3_shxxmfhtk2bc4pbx5cyq3uoph4 react-i18next: 11.18.3_shxxmfhtk2bc4pbx5cyq3uoph4
react-modal: 3.15.1_biqbaboplfbrettd7655fr4n2y react-modal: 3.15.1_biqbaboplfbrettd7655fr4n2y
react-router-dom: 6.2.2_biqbaboplfbrettd7655fr4n2y react-router-dom: 6.2.2_biqbaboplfbrettd7655fr4n2y