* feat: wip react-hook-form * feat: use react-hook-form for password login * feat: clean regex * feat: add react-hook-form on create workspace * feat: add react-hook-form on create profile page * fix: clean rebased code * fix: rebase issue * fix: add new stories to go over 65% --------- Co-authored-by: Charles Bochet <charles@twenty.com>
215 lines
6.0 KiB
TypeScript
215 lines
6.0 KiB
TypeScript
import { useCallback, useState } from 'react';
|
|
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
import styled from '@emotion/styled';
|
|
import { yupResolver } from '@hookform/resolvers/yup';
|
|
import { useRecoilState } from 'recoil';
|
|
import * as Yup from 'yup';
|
|
|
|
import { Logo } from '@/auth/components/ui/Logo';
|
|
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
|
import { Title } from '@/auth/components/ui/Title';
|
|
import { useAuth } from '@/auth/hooks/useAuth';
|
|
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
|
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
|
|
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
|
|
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
|
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
|
import { MainButton } from '@/ui/components/buttons/MainButton';
|
|
import { TextInput } from '@/ui/components/inputs/TextInput';
|
|
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
|
import { useCheckUserExistsQuery } from '~/generated/graphql';
|
|
|
|
const StyledContentContainer = styled.div`
|
|
width: 100%;
|
|
> * + * {
|
|
margin-top: ${({ theme }) => theme.spacing(6)};
|
|
}
|
|
`;
|
|
|
|
const StyledForm = styled.form`
|
|
align-items: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
> * + * {
|
|
margin-top: ${({ theme }) => theme.spacing(8)};
|
|
}
|
|
`;
|
|
|
|
const StyledSectionContainer = styled.div`
|
|
> * + * {
|
|
margin-top: ${({ theme }) => theme.spacing(4)};
|
|
}
|
|
`;
|
|
|
|
const StyledButtonContainer = styled.div`
|
|
width: 200px;
|
|
`;
|
|
|
|
const StyledErrorContainer = styled.div`
|
|
color: ${({ theme }) => theme.color.red};
|
|
`;
|
|
|
|
const validationSchema = Yup.object()
|
|
.shape({
|
|
exist: Yup.boolean().required(),
|
|
email: Yup.string().email('Email must be a valid email').required(),
|
|
password: Yup.string()
|
|
.matches(
|
|
PASSWORD_REGEX,
|
|
'Password must contain at least 8 characters, one uppercase and one number',
|
|
)
|
|
.required(),
|
|
})
|
|
.required();
|
|
|
|
type Form = Yup.InferType<typeof validationSchema>;
|
|
|
|
export function PasswordLogin() {
|
|
const navigate = useNavigate();
|
|
|
|
const [isDemoMode] = useRecoilState(isDemoModeState);
|
|
const [authFlowUserEmail] = useRecoilState(authFlowUserEmailState);
|
|
const [showErrors, setShowErrors] = useState(false);
|
|
|
|
const workspaceInviteHash = useParams().workspaceInviteHash;
|
|
|
|
const { data: checkUserExistsData } = useCheckUserExistsQuery({
|
|
variables: {
|
|
email: authFlowUserEmail,
|
|
},
|
|
});
|
|
|
|
const { login, signUp } = useAuth();
|
|
|
|
// Form
|
|
const {
|
|
control,
|
|
handleSubmit,
|
|
formState: { isSubmitting },
|
|
setError,
|
|
watch,
|
|
getValues,
|
|
} = useForm<Form>({
|
|
mode: 'onChange',
|
|
defaultValues: {
|
|
exist: false,
|
|
email: authFlowUserEmail,
|
|
password: isDemoMode ? 'Applecar2025' : '',
|
|
},
|
|
resolver: yupResolver(validationSchema),
|
|
});
|
|
|
|
const onSubmit: SubmitHandler<Form> = useCallback(
|
|
async (data) => {
|
|
try {
|
|
if (!data.email || !data.password) {
|
|
throw new Error('Email and password are required');
|
|
}
|
|
if (checkUserExistsData?.checkUserExists.exists) {
|
|
await login(data.email, data.password);
|
|
} else {
|
|
await signUp(data.email, data.password, workspaceInviteHash);
|
|
}
|
|
navigate('/auth/create/workspace');
|
|
} catch (err: any) {
|
|
setError('root', { message: err?.message });
|
|
}
|
|
},
|
|
[
|
|
login,
|
|
navigate,
|
|
setError,
|
|
signUp,
|
|
workspaceInviteHash,
|
|
checkUserExistsData,
|
|
],
|
|
);
|
|
useScopedHotkeys(
|
|
'enter',
|
|
() => {
|
|
onSubmit(getValues());
|
|
},
|
|
InternalHotkeysScope.PasswordLogin,
|
|
[onSubmit],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<Logo />
|
|
<Title>Welcome to Twenty</Title>
|
|
<SubTitle>
|
|
Enter your credentials to sign{' '}
|
|
{checkUserExistsData?.checkUserExists.exists ? 'in' : 'up'}
|
|
</SubTitle>
|
|
<StyledForm
|
|
onSubmit={(event) => {
|
|
setShowErrors(true);
|
|
return handleSubmit(onSubmit)(event);
|
|
}}
|
|
>
|
|
<StyledContentContainer>
|
|
<StyledSectionContainer>
|
|
<SubSectionTitle title="Email" />
|
|
<Controller
|
|
name="email"
|
|
control={control}
|
|
render={({
|
|
field: { onChange, onBlur, value },
|
|
fieldState: { error },
|
|
}) => (
|
|
<TextInput
|
|
value={value}
|
|
placeholder="Email"
|
|
onBlur={onBlur}
|
|
onChange={onChange}
|
|
error={showErrors ? error?.message : undefined}
|
|
fullWidth
|
|
/>
|
|
)}
|
|
/>
|
|
</StyledSectionContainer>
|
|
<StyledSectionContainer>
|
|
<SubSectionTitle title="Password" />
|
|
<Controller
|
|
name="password"
|
|
control={control}
|
|
render={({
|
|
field: { onChange, onBlur, value },
|
|
fieldState: { error },
|
|
}) => (
|
|
<TextInput
|
|
value={value}
|
|
type="password"
|
|
placeholder="Password"
|
|
onBlur={onBlur}
|
|
onChange={onChange}
|
|
error={showErrors ? error?.message : undefined}
|
|
fullWidth
|
|
/>
|
|
)}
|
|
/>
|
|
</StyledSectionContainer>
|
|
</StyledContentContainer>
|
|
<StyledButtonContainer>
|
|
<MainButton
|
|
title="Continue"
|
|
type="submit"
|
|
disabled={!watch('email') || !watch('password') || isSubmitting}
|
|
fullWidth
|
|
/>
|
|
</StyledButtonContainer>
|
|
{/* Will be replaced by error snack bar */}
|
|
<Controller
|
|
name="exist"
|
|
control={control}
|
|
render={({ formState: { errors } }) => (
|
|
<StyledErrorContainer>{errors?.root?.message}</StyledErrorContainer>
|
|
)}
|
|
/>
|
|
</StyledForm>
|
|
</>
|
|
);
|
|
}
|