refactor(auth): integrate react-hook-form context (#9417)

Remove redundant validation logic and streamline form handling by
leveraging react-hook-form's context. Simplify component props and
enhance consistency across the sign-in/up flow.

Fix https://github.com/twentyhq/twenty/issues/9380
This commit is contained in:
Antoine Moreaux
2025-01-07 17:09:45 +01:00
committed by GitHub
parent 6129052850
commit 6e04a3b19f
4 changed files with 37 additions and 46 deletions

View File

@ -2,7 +2,6 @@ import { TextInput } from '@/ui/input/components/TextInput';
import { Controller, useFormContext } from 'react-hook-form'; import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { isDefined } from '~/utils/isDefined';
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
const StyledFullWidthMotionDiv = styled(motion.div)` const StyledFullWidthMotionDiv = styled(motion.div)`
@ -13,13 +12,7 @@ const StyledInputContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(3)}; margin-bottom: ${({ theme }) => theme.spacing(3)};
`; `;
export const SignInUpEmailField = ({ export const SignInUpEmailField = ({ showErrors }: { showErrors: boolean }) => {
showErrors,
onChange: onChangeFromProps,
}: {
showErrors: boolean;
onChange?: (value: string) => void;
}) => {
const form = useFormContext<Form>(); const form = useFormContext<Form>();
return ( return (
@ -45,10 +38,7 @@ export const SignInUpEmailField = ({
value={value} value={value}
placeholder="Email" placeholder="Email"
onBlur={onBlur} onBlur={onBlur}
onChange={(value: string) => { onChange={onChange}
onChange(value);
if (isDefined(onChangeFromProps)) onChangeFromProps(value);
}}
error={showErrors ? error?.message : undefined} error={showErrors ? error?.message : undefined}
fullWidth fullWidth
/> />

View File

@ -3,17 +3,18 @@ import {
signInUpStepState, signInUpStepState,
} from '@/auth/states/signInUpStepState'; } from '@/auth/states/signInUpStepState';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp'; import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { Loader, MainButton } from 'twenty-ui'; import { Loader, MainButton } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField'; import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField'; import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState'; import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { FormProvider } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { SignInUpMode } from '@/auth/types/signInUpMode'; import { SignInUpMode } from '@/auth/types/signInUpMode';
const StyledForm = styled.form` const StyledForm = styled.form`
@ -24,7 +25,7 @@ const StyledForm = styled.form`
`; `;
export const SignInUpWithCredentials = () => { export const SignInUpWithCredentials = () => {
const { form, validationSchema } = useSignInUpForm(); const form = useFormContext<Form>();
const signInUpStep = useRecoilValue(signInUpStepState); const signInUpStep = useRecoilValue(signInUpStepState);
const [showErrors, setShowErrors] = useState(false); const [showErrors, setShowErrors] = useState(false);
@ -90,8 +91,7 @@ export const SignInUpWithCredentials = () => {
const isEmailStepSubmitButtonDisabledCondition = const isEmailStepSubmitButtonDisabledCondition =
signInUpStep === SignInUpStep.Email && signInUpStep === SignInUpStep.Email &&
(!validationSchema.shape.email.safeParse(form.watch('email')).success || (isDefined(form.formState.errors['email']) || shouldWaitForCaptchaToken);
shouldWaitForCaptchaToken);
// TODO: isValid is actually a proxy function. If it is not rendered the first time, react might not trigger re-renders // TODO: isValid is actually a proxy function. If it is not rendered the first time, react might not trigger re-renders
// We make the isValid check synchronous and update a reactState to make sure this does not happen // We make the isValid check synchronous and update a reactState to make sure this does not happen
@ -110,32 +110,27 @@ export const SignInUpWithCredentials = () => {
{(signInUpStep === SignInUpStep.Password || {(signInUpStep === SignInUpStep.Password ||
signInUpStep === SignInUpStep.Email || signInUpStep === SignInUpStep.Email ||
signInUpStep === SignInUpStep.Init) && ( signInUpStep === SignInUpStep.Init) && (
<> <StyledForm onSubmit={handleSubmit}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */} {signInUpStep !== SignInUpStep.Init && (
<FormProvider {...form}> <SignInUpEmailField showErrors={showErrors} />
<StyledForm onSubmit={handleSubmit}> )}
{signInUpStep !== SignInUpStep.Init && ( {signInUpStep === SignInUpStep.Password && (
<SignInUpEmailField showErrors={showErrors} /> <SignInUpPasswordField
)} showErrors={showErrors}
{signInUpStep === SignInUpStep.Password && ( signInUpMode={signInUpMode}
<SignInUpPasswordField />
showErrors={showErrors} )}
signInUpMode={signInUpMode} <MainButton
/> title={buttonTitle}
)} type="submit"
<MainButton variant={
title={buttonTitle} signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary'
type="submit" }
variant={ Icon={() => (form.formState.isSubmitting ? <Loader /> : null)}
signInUpStep === SignInUpStep.Init ? 'secondary' : 'primary' disabled={isSubmitButtonDisabled}
} fullWidth
Icon={() => (form.formState.isSubmitting ? <Loader /> : null)} />
disabled={isSubmitButtonDisabled} </StyledForm>
fullWidth
/>
</StyledForm>
</FormProvider>
</>
)} )}
</> </>
); );

View File

@ -8,6 +8,7 @@ import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SignInUpStep } from '@/auth/states/signInUpStepState'; import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { FormProvider } from 'react-hook-form';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { ActionLink, HorizontalSeparator } from 'twenty-ui'; import { ActionLink, HorizontalSeparator } from 'twenty-ui';
@ -20,6 +21,7 @@ export const SignInUpWorkspaceScopeForm = () => {
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState); const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const { form } = useSignInUpForm(); const { form } = useSignInUpForm();
const { handleResetPassword } = useHandleResetPassword(); const { handleResetPassword } = useHandleResetPassword();
const { signInUpStep } = useSignInUp(form); const { signInUpStep } = useSignInUp(form);
@ -39,8 +41,12 @@ export const SignInUpWorkspaceScopeForm = () => {
workspaceAuthProviders.password ? ( workspaceAuthProviders.password ? (
<HorizontalSeparator /> <HorizontalSeparator />
) : null} ) : null}
{workspaceAuthProviders.password && (
{workspaceAuthProviders.password && <SignInUpWithCredentials />} // eslint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...form}>
<SignInUpWithCredentials />
</FormProvider>
)}
</StyledContentContainer> </StyledContentContainer>
{signInUpStep === SignInUpStep.Password && ( {signInUpStep === SignInUpStep.Password && (
<ActionLink onClick={handleResetPassword(form.getValues('email'))}> <ActionLink onClick={handleResetPassword(form.getValues('email'))}>

View File

@ -70,5 +70,5 @@ export const useSignInUpForm = () => {
prefilledEmail, prefilledEmail,
location.search, location.search,
]); ]);
return { form: form, validationSchema }; return { form: form };
}; };