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:
martmull
2024-06-05 18:16:53 +02:00
committed by GitHub
parent fda0d2a170
commit 9f6a6c3282
92 changed files with 2707 additions and 1246 deletions

View File

@ -1,6 +1,5 @@
import { useCallback } from 'react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSetRecoilState } from 'recoil';
@ -16,7 +15,6 @@ import { OnboardingStatus } from '@/auth/utils/getOnboardingStatus';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { AppPath } from '@/types/AppPath';
import { Loader } from '@/ui/feedback/loader/components/Loader';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@ -47,8 +45,6 @@ const validationSchema = z
type Form = z.infer<typeof validationSchema>;
export const CreateWorkspace = () => {
const navigate = useNavigate();
const { enqueueSnackBar } = useSnackBar();
const onboardingStatus = useOnboardingStatus();
@ -88,10 +84,6 @@ export const CreateWorkspace = () => {
if (isDefined(result.errors)) {
throw result.errors ?? new Error('Unknown error');
}
setTimeout(() => {
navigate(AppPath.CreateProfile);
}, 20);
} catch (error: any) {
enqueueSnackBar(error?.message, {
variant: SnackBarVariant.Error,
@ -102,7 +94,6 @@ export const CreateWorkspace = () => {
activateWorkspace,
setIsCurrentUserLoaded,
apolloMetadataClient,
navigate,
enqueueSnackBar,
],
);

View File

@ -0,0 +1,94 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { IconGoogle } from 'twenty-ui';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
import { OnboardingSyncEmailsSettingsCard } from '@/onboarding/components/OnboardingSyncEmailsSettingsCard';
import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
import { AppPath } from '@/types/AppPath';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
import {
CalendarChannelVisibility,
MessageChannelVisibility,
useSkipSyncEmailOnboardingStepMutation,
} from '~/generated/graphql';
const StyledSyncEmailsContainer = styled.div`
display: flex;
flex-direction: row;
width: 100%;
margin: ${({ theme }) => theme.spacing(8)} 0;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledActionLinkContainer = styled.div`
display: flex;
flex-direction: row;
margin: ${({ theme }) => theme.spacing(3)} 0 0;
`;
export const SyncEmails = () => {
const theme = useTheme();
const navigate = useNavigate();
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
const setIsCurrentUserLoaded = useSetRecoilState(isCurrentUserLoadedState);
const [visibility, setVisibility] = useState<MessageChannelVisibility>(
MessageChannelVisibility.ShareEverything,
);
const [skipSyncEmailOnboardingStepMutation] =
useSkipSyncEmailOnboardingStepMutation();
const handleButtonClick = async () => {
const calendarChannelVisibility =
visibility === MessageChannelVisibility.ShareEverything
? CalendarChannelVisibility.ShareEverything
: CalendarChannelVisibility.Metadata;
await triggerGoogleApisOAuth(
AppPath.Index,
visibility,
calendarChannelVisibility,
);
};
const continueWithoutSync = async () => {
await skipSyncEmailOnboardingStepMutation();
setIsCurrentUserLoaded(false);
navigate(AppPath.Index);
};
const isSubmitting = false;
return (
<>
<Title withMarginTop={false}>Emails and Calendar</Title>
<SubTitle>
Sync your Emails and Calendar with Twenty. Choose your privacy settings.
</SubTitle>
<StyledSyncEmailsContainer>
<OnboardingSyncEmailsSettingsCard
value={visibility}
onChange={setVisibility}
/>
</StyledSyncEmailsContainer>
<MainButton
title="Sync with Google"
onClick={handleButtonClick}
width={200}
Icon={() => <IconGoogle size={theme.icon.size.sm} />}
disabled={isSubmitting}
/>
<StyledActionLinkContainer>
<ActionLink onClick={continueWithoutSync}>
Continue without sync
</ActionLink>
</StyledActionLinkContainer>
</>
);
};

View File

@ -5,6 +5,7 @@ import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan';
import {
PageDecorator,
PageDecoratorArgs,
@ -13,10 +14,8 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { sleep } from '~/testing/sleep';
import { ChooseYourPlan } from '../ChooseYourPlan';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/ChooseYourPlan',
title: 'Pages/Onboarding/ChooseYourPlan',
component: ChooseYourPlan,
decorators: [PageDecorator],
args: { routePath: AppPath.PlanRequired },

View File

@ -5,6 +5,7 @@ import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { CreateProfile } from '~/pages/onboarding/CreateProfile';
import {
PageDecorator,
PageDecoratorArgs,
@ -12,10 +13,8 @@ import {
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { CreateProfile } from '../CreateProfile';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/CreateProfile',
title: 'Pages/Onboarding/CreateProfile',
component: CreateProfile,
decorators: [PageDecorator],
args: { routePath: AppPath.CreateProfile },

View File

@ -7,6 +7,7 @@ import { useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace';
import {
PageDecorator,
PageDecoratorArgs,
@ -14,10 +15,8 @@ import {
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { CreateWorkspace } from '../CreateWorkspace';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/CreateWorkspace',
title: 'Pages/Onboarding/CreateWorkspace',
component: CreateWorkspace,
decorators: [
(Story) => {

View File

@ -5,6 +5,7 @@ import { graphql, HttpResponse } from 'msw';
import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess';
import {
PageDecorator,
PageDecoratorArgs,
@ -12,10 +13,8 @@ import {
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
import { PaymentSuccess } from '../PaymentSuccess';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Auth/PaymentSuccess',
title: 'Pages/Onboarding/PaymentSuccess',
component: PaymentSuccess,
decorators: [PageDecorator],
args: { routePath: AppPath.PlanRequiredSuccess },

View File

@ -0,0 +1,46 @@
import { getOperationName } from '@apollo/client/utilities';
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw';
import { AppPath } from '~/modules/types/AppPath';
import { GET_CURRENT_USER } from '~/modules/users/graphql/queries/getCurrentUser';
import { SyncEmails } from '~/pages/onboarding/SyncEmails';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Onboarding/SyncEmails',
component: SyncEmails,
decorators: [PageDecorator],
args: { routePath: AppPath.SyncEmails },
parameters: {
msw: {
handlers: [
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
return HttpResponse.json({
data: {
currentUser: mockedOnboardingUsersData[0],
},
});
}),
graphqlMocks.handlers,
],
},
},
};
export default meta;
export type Story = StoryObj<typeof SyncEmails>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Emails and Calendar');
},
};

View File

@ -17,10 +17,8 @@ import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import {
TimelineCalendarEvent,
TimelineCalendarEventVisibility,
} from '~/generated-metadata/graphql';
import { CalendarChannelVisibility } from '~/generated/graphql';
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
export const SettingsAccountsCalendars = () => {
const calendarSettingsEnabled = false;
@ -79,7 +77,7 @@ export const SettingsAccountsCalendars = () => {
isCanceled: false,
location: '',
title: 'Onboarding call',
visibility: TimelineCalendarEventVisibility.ShareEverything,
visibility: CalendarChannelVisibility.ShareEverything,
};
return (

View File

@ -4,10 +4,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { H2Title, IconRefresh, IconSettings, IconUser } from 'twenty-ui';
import {
CalendarChannel,
CalendarChannelVisibility,
} from '@/accounts/types/CalendarChannel';
import { CalendarChannel } from '@/accounts/types/CalendarChannel';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
@ -21,6 +18,7 @@ import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { CalendarChannelVisibility } from '~/generated/graphql';
const StyledCardMedia = styled(SettingsAccountsCardMedia)`
height: ${({ theme }) => theme.spacing(6)};

View File

@ -8,16 +8,14 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SettingsAccountsCardMedia } from '@/settings/accounts/components/SettingsAccountsCardMedia';
import {
InboxSettingsVisibilityValue,
SettingsAccountsInboxVisibilitySettingsCard,
} from '@/settings/accounts/components/SettingsAccountsInboxVisibilitySettingsCard';
import { SettingsAccountsInboxVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsInboxVisibilitySettingsCard';
import { SettingsAccountsToggleSettingCard } from '@/settings/accounts/components/SettingsAccountsToggleSettingCard';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { AppPath } from '@/types/AppPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { MessageChannelVisibility } from '~/generated/graphql';
export const SettingsAccountsEmailsInboxSettings = () => {
const theme = useTheme();
@ -33,7 +31,7 @@ export const SettingsAccountsEmailsInboxSettings = () => {
objectNameSingular: CoreObjectNameSingular.MessageChannel,
});
const handleVisibilityChange = (value: InboxSettingsVisibilityValue) => {
const handleVisibilityChange = (value: MessageChannelVisibility) => {
updateOneRecord({
idToUpdate: messageChannelId,
updateOneRecordInput: {

View File

@ -2,6 +2,7 @@ import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import { graphql, HttpResponse } from 'msw';
import { MessageChannelVisibility } from '~/generated/graphql';
import { SettingsAccountsEmailsInboxSettings } from '~/pages/settings/accounts/SettingsAccountsEmailsInboxSettings';
import {
PageDecorator,
@ -26,7 +27,7 @@ const meta: Meta<PageDecoratorArgs> = {
data: {
messageChannel: {
id: '1',
visibility: 'share_everything',
visibility: MessageChannelVisibility.ShareEverything,
messageThreads: { edges: [] },
createdAt: '2021-08-27T12:00:00Z',
type: 'email',