5622 add a syncemail onboarding step (#5689)
- add sync email onboarding step - refactor calendar and email visibility enums - add a new table `keyValuePair` in `core` schema - add a new resolved boolean field `skipSyncEmail` in current user https://github.com/twentyhq/twenty/assets/29927851/de791475-5bfe-47f9-8e90-76c349fba56f
This commit is contained in:
220
packages/twenty-front/src/pages/onboarding/CreateProfile.tsx
Normal file
220
packages/twenty-front/src/pages/onboarding/CreateProfile.tsx
Normal file
@ -0,0 +1,220 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import styled from '@emotion/styled';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { H2Title } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SubTitle } from '@/auth/components/SubTitle';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { useOnboardingStatus } from '@/auth/hooks/useOnboardingStatus';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader';
|
||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSectionContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
width: 200px;
|
||||
`;
|
||||
|
||||
const StyledComboInputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
> * + * {
|
||||
margin-left: ${({ theme }) => theme.spacing(4)};
|
||||
}
|
||||
`;
|
||||
|
||||
const validationSchema = z
|
||||
.object({
|
||||
firstName: z.string().min(1, { message: 'First name can not be empty' }),
|
||||
lastName: z.string().min(1, { message: 'Last name can not be empty' }),
|
||||
})
|
||||
.required();
|
||||
|
||||
type Form = z.infer<typeof validationSchema>;
|
||||
|
||||
export const CreateProfile = () => {
|
||||
const onboardingStatus = useOnboardingStatus();
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord<WorkspaceMember>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
});
|
||||
|
||||
// Form
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isValid, isSubmitting },
|
||||
getValues,
|
||||
} = useForm<Form>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
firstName: currentWorkspaceMember?.name?.firstName ?? '',
|
||||
lastName: currentWorkspaceMember?.name?.lastName ?? '',
|
||||
},
|
||||
resolver: zodResolver(validationSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<Form> = useCallback(
|
||||
async (data) => {
|
||||
try {
|
||||
if (!currentWorkspaceMember?.id) {
|
||||
throw new Error('User is not logged in');
|
||||
}
|
||||
if (!data.firstName || !data.lastName) {
|
||||
throw new Error('First name or last name is missing');
|
||||
}
|
||||
|
||||
await updateOneRecord({
|
||||
idToUpdate: currentWorkspaceMember?.id,
|
||||
updateOneRecordInput: {
|
||||
name: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
},
|
||||
colorScheme: 'System',
|
||||
},
|
||||
});
|
||||
|
||||
setCurrentWorkspaceMember(
|
||||
(current) =>
|
||||
({
|
||||
...current,
|
||||
name: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
},
|
||||
colorScheme: 'System',
|
||||
}) as any,
|
||||
);
|
||||
} catch (error: any) {
|
||||
enqueueSnackBar(error?.message, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
currentWorkspaceMember?.id,
|
||||
enqueueSnackBar,
|
||||
setCurrentWorkspaceMember,
|
||||
updateOneRecord,
|
||||
],
|
||||
);
|
||||
|
||||
const [isEditingMode, setIsEditingMode] = useState(false);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
if (isEditingMode) {
|
||||
onSubmit(getValues());
|
||||
}
|
||||
},
|
||||
PageHotkeyScope.CreateProfile,
|
||||
);
|
||||
|
||||
if (onboardingStatus !== OnboardingStatus.OngoingProfileCreation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title withMarginTop={false}>Create profile</Title>
|
||||
<SubTitle>How you'll be identified on the app.</SubTitle>
|
||||
<StyledContentContainer>
|
||||
<StyledSectionContainer>
|
||||
<H2Title title="Picture" />
|
||||
<ProfilePictureUploader />
|
||||
</StyledSectionContainer>
|
||||
<StyledSectionContainer>
|
||||
<H2Title
|
||||
title="Name"
|
||||
description="Your name as it will be displayed on the app"
|
||||
/>
|
||||
{/* TODO: When react-web-hook-form is added to edit page we should create a dedicated component with context */}
|
||||
<StyledComboInputContainer>
|
||||
<Controller
|
||||
name="firstName"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<TextInputV2
|
||||
autoFocus
|
||||
label="First Name"
|
||||
value={value}
|
||||
onFocus={() => setIsEditingMode(true)}
|
||||
onBlur={() => {
|
||||
onBlur();
|
||||
setIsEditingMode(false);
|
||||
}}
|
||||
onChange={onChange}
|
||||
placeholder="Tim"
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="lastName"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<TextInputV2
|
||||
label="Last Name"
|
||||
value={value}
|
||||
onFocus={() => setIsEditingMode(true)}
|
||||
onBlur={() => {
|
||||
onBlur();
|
||||
setIsEditingMode(false);
|
||||
}}
|
||||
onChange={onChange}
|
||||
placeholder="Cook"
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledComboInputContainer>
|
||||
</StyledSectionContainer>
|
||||
</StyledContentContainer>
|
||||
<StyledButtonContainer>
|
||||
<MainButton
|
||||
title="Continue"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
disabled={!isValid || isSubmitting}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user