5623 add an inviteteam onboarding step (#5769)

## Changes
- add a new invite Team onboarding step
- update currentUser.state to currentUser.onboardingStep

## Edge cases
We will never display invite team onboarding step 
- if number of workspaceMember > 1
- if a workspaceMember as been deleted

## Important changes
Update typeorm package version to 0.3.20 because we needed a fix on
`indexPredicates` pushed in 0.3.20 version
(https://github.com/typeorm/typeorm/issues/10191)

## Result
<img width="844" alt="image"
src="https://github.com/twentyhq/twenty/assets/29927851/0dab54cf-7c66-4c64-b0c9-b0973889a148">



https://github.com/twentyhq/twenty/assets/29927851/13268d0a-cfa7-42a4-84c6-9e1fbbe48912
This commit is contained in:
martmull
2024-06-12 21:13:18 +02:00
committed by GitHub
parent 2fdd2f4949
commit 3986824017
60 changed files with 1009 additions and 372 deletions

View File

@ -5,30 +5,30 @@ import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEase
type TitleProps = React.PropsWithChildren & {
animate?: boolean;
withMarginTop?: boolean;
noMarginTop?: boolean;
};
const StyledTitle = styled.div<Pick<TitleProps, 'withMarginTop'>>`
const StyledTitle = styled.div<Pick<TitleProps, 'noMarginTop'>>`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.xl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(4)};
margin-top: ${({ theme, withMarginTop }) =>
withMarginTop ? theme.spacing(4) : 0};
margin-top: ${({ theme, noMarginTop }) =>
!noMarginTop ? theme.spacing(4) : 0};
`;
export const Title = ({
children,
animate = false,
withMarginTop = true,
noMarginTop = false,
}: TitleProps) => {
if (animate) {
return (
<StyledTitle withMarginTop={withMarginTop}>
<StyledTitle noMarginTop={noMarginTop}>
<AnimatedEaseIn>{children}</AnimatedEaseIn>
</StyledTitle>
);
}
return <StyledTitle withMarginTop={withMarginTop}>{children}</StyledTitle>;
return <StyledTitle noMarginTop={noMarginTop}>{children}</StyledTitle>;
};

View File

@ -12,6 +12,7 @@ import {
import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { billingState } from '@/client-config/states/billingState';
import { OnboardingStep } from '~/generated/graphql';
const tokenPair = {
accessToken: { token: 'accessToken', expiresAt: 'expiresAt' },
@ -26,7 +27,7 @@ const currentUser = {
email: 'test@test',
supportUserHash: '1',
canImpersonate: false,
state: { skipSyncEmailOnboardingStep: true },
onboardingStep: null,
} as CurrentUser;
const currentWorkspace = {
activationStatus: 'active',
@ -196,7 +197,7 @@ describe('useOnboardingStatus', () => {
setBilling(billing);
setCurrentUser({
...currentUser,
state: { skipSyncEmailOnboardingStep: false },
onboardingStep: OnboardingStep.SyncEmail,
});
setCurrentWorkspace({
...currentWorkspace,
@ -214,6 +215,39 @@ describe('useOnboardingStatus', () => {
expect(result.current.onboardingStatus).toBe('ongoing_sync_email');
});
it('should return "ongoing_invite_team"', async () => {
const { result } = renderHooks();
const {
setTokenPair,
setBilling,
setCurrentUser,
setCurrentWorkspace,
setCurrentWorkspaceMember,
} = result.current;
act(() => {
setTokenPair(tokenPair);
setBilling(billing);
setCurrentUser({
...currentUser,
onboardingStep: OnboardingStep.InviteTeam,
});
setCurrentWorkspace({
...currentWorkspace,
subscriptionStatus: 'active',
});
setCurrentWorkspaceMember({
...currentWorkspaceMember,
name: {
firstName: 'John',
lastName: 'Doe',
},
});
});
expect(result.current.onboardingStatus).toBe('ongoing_invite_team');
});
it('should return "completed"', async () => {
const { result } = renderHooks();
const {

View File

@ -4,6 +4,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { IconGoogle, IconMicrosoft } from 'twenty-ui';
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
@ -69,7 +70,7 @@ export const SignInUpForm = () => {
const handleKeyDown = async (
event: React.KeyboardEvent<HTMLInputElement>,
) => {
if (event.key === 'Enter') {
if (event.key === Key.Enter) {
event.preventDefault();
if (signInUpStep === SignInUpStep.Init) {

View File

@ -4,7 +4,7 @@ import { User } from '~/generated/graphql';
export type CurrentUser = Pick<
User,
'id' | 'email' | 'supportUserHash' | 'canImpersonate' | 'state'
'id' | 'email' | 'supportUserHash' | 'canImpersonate' | 'onboardingStep'
>;
export const currentUserState = createState<CurrentUser | null>({

View File

@ -1,6 +1,7 @@
import { CurrentUser } from '@/auth/states/currentUserState';
import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStep } from '~/generated/graphql';
import { getOnboardingStatus } from '../getOnboardingStatus';
@ -22,7 +23,7 @@ describe('getOnboardingStatus', () => {
activationStatus: 'inactive',
} as CurrentWorkspace,
currentUser: {
state: { skipSyncEmailOnboardingStep: true },
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
@ -38,7 +39,7 @@ describe('getOnboardingStatus', () => {
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
state: { skipSyncEmailOnboardingStep: true },
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
@ -57,7 +58,26 @@ describe('getOnboardingStatus', () => {
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
state: { skipSyncEmailOnboardingStep: false },
onboardingStep: OnboardingStep.SyncEmail,
} as CurrentUser,
isBillingEnabled: false,
});
const ongoingInviteTeam = getOnboardingStatus({
isLoggedIn: true,
currentWorkspaceMember: {
id: '1',
name: {
firstName: 'John',
lastName: 'Doe',
},
} as WorkspaceMember,
currentWorkspace: {
id: '1',
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
onboardingStep: OnboardingStep.InviteTeam,
} as CurrentUser,
isBillingEnabled: false,
});
@ -76,7 +96,7 @@ describe('getOnboardingStatus', () => {
activationStatus: 'active',
} as CurrentWorkspace,
currentUser: {
state: { skipSyncEmailOnboardingStep: true },
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
@ -96,7 +116,7 @@ describe('getOnboardingStatus', () => {
subscriptionStatus: 'incomplete',
} as CurrentWorkspace,
currentUser: {
state: { skipSyncEmailOnboardingStep: true },
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: true,
});
@ -116,7 +136,7 @@ describe('getOnboardingStatus', () => {
subscriptionStatus: 'incomplete',
} as CurrentWorkspace,
currentUser: {
state: { skipSyncEmailOnboardingStep: true },
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: false,
});
@ -136,7 +156,7 @@ describe('getOnboardingStatus', () => {
subscriptionStatus: 'canceled',
} as CurrentWorkspace,
currentUser: {
state: { skipSyncEmailOnboardingStep: true },
onboardingStep: null,
} as CurrentUser,
isBillingEnabled: true,
});
@ -145,6 +165,7 @@ describe('getOnboardingStatus', () => {
expect(ongoingWorkspaceActivation).toBe('ongoing_workspace_activation');
expect(ongoingProfileCreation).toBe('ongoing_profile_creation');
expect(ongoingSyncEmail).toBe('ongoing_sync_email');
expect(ongoingInviteTeam).toBe('ongoing_invite_team');
expect(completed).toBe('completed');
expect(incomplete).toBe('incomplete');
expect(canceled).toBe('canceled');

View File

@ -1,6 +1,7 @@
import { CurrentUser } from '@/auth/states/currentUserState';
import { CurrentWorkspace } from '@/auth/states/currentWorkspaceState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStep } from '~/generated/graphql';
export enum OnboardingStatus {
Incomplete = 'incomplete',
@ -11,6 +12,7 @@ export enum OnboardingStatus {
OngoingWorkspaceActivation = 'ongoing_workspace_activation',
OngoingProfileCreation = 'ongoing_profile_creation',
OngoingSyncEmail = 'ongoing_sync_email',
OngoingInviteTeam = 'ongoing_invite_team',
Completed = 'completed',
CompletedWithoutSubscription = 'completed_without_subscription',
}
@ -59,10 +61,14 @@ export const getOnboardingStatus = ({
return OnboardingStatus.OngoingProfileCreation;
}
if (!currentUser.state.skipSyncEmailOnboardingStep) {
if (currentUser.onboardingStep === OnboardingStep.SyncEmail) {
return OnboardingStatus.OngoingSyncEmail;
}
if (currentUser.onboardingStep === OnboardingStep.InviteTeam) {
return OnboardingStatus.OngoingInviteTeam;
}
if (isBillingEnabled && currentWorkspace.subscriptionStatus === 'canceled') {
return OnboardingStatus.Canceled;
}

View File

@ -0,0 +1,41 @@
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { OnboardingStep } from '~/generated/graphql';
const getNextOnboardingStep = (
currentOnboardingStep: OnboardingStep,
workspaceMembers: WorkspaceMember[],
) => {
if (currentOnboardingStep === OnboardingStep.SyncEmail) {
return workspaceMembers && workspaceMembers.length > 1
? null
: OnboardingStep.InviteTeam;
}
return null;
};
export const useSetNextOnboardingStep = () => {
const setCurrentUser = useSetRecoilState(currentUserState);
const { records: workspaceMembers } = useFindManyRecords<WorkspaceMember>({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
});
return useRecoilCallback(
() => (currentOnboardingStep: OnboardingStep) => {
setCurrentUser(
(current) =>
({
...current,
onboardingStep: getNextOnboardingStep(
currentOnboardingStep,
workspaceMembers,
),
}) as any,
);
},
[setCurrentUser, workspaceMembers],
);
};

View File

@ -9,6 +9,7 @@ export enum AppPath {
CreateWorkspace = '/create/workspace',
CreateProfile = '/create/profile',
SyncEmails = '/sync/emails',
InviteTeam = '/invite-team',
PlanRequired = '/plan-required',
PlanRequiredSuccess = '/plan-required/payment-success',

View File

@ -3,6 +3,8 @@ export enum PageHotkeyScope {
CreateWokspace = 'create-workspace',
SignInUp = 'sign-in-up',
CreateProfile = 'create-profile',
InviteTeam = 'invite-team',
SyncEmail = 'sync-email',
PlanRequired = 'plan-required',
ShowPage = 'show-page',
PersonShowPage = 'person-show-page',

View File

@ -0,0 +1,34 @@
import React from 'react';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
display: flex;
align-items: center;
width: 100%;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
color: ${({ theme }) => theme.font.color.extraLight};
&:before,
&:after {
content: '';
height: 1px;
flex-grow: 1;
background: ${({ theme }) => theme.background.transparent.light};
}
&:before {
margin: 0 ${({ theme }) => theme.spacing(4)} 0 0;
}
&:after {
margin: 0 0 0 ${({ theme }) => theme.spacing(4)};
}
`;
export const SeparatorLineText = ({
children,
}: {
children: React.ReactNode;
}) => {
return <StyledContainer>{children}</StyledContainer>;
};

View File

@ -0,0 +1,16 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
import { SeparatorLineText } from '../SeparatorLineText';
const meta: Meta<typeof SeparatorLineText> = {
title: 'UI/Display/Text/SeparatorLineText',
component: SeparatorLineText,
args: { children: 'Or' },
decorators: [ComponentDecorator],
};
export default meta;
type Story = StoryObj<typeof SeparatorLineText>;
export const Default: Story = {};

View File

@ -117,6 +117,7 @@ export type TextInputV2ComponentProps = Omit<
onChange?: (text: string) => void;
fullWidth?: boolean;
error?: string;
noErrorHelper?: boolean;
RightIcon?: IconComponent;
LeftIcon?: IconComponent;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
@ -134,6 +135,7 @@ const TextInputV2Component = (
onKeyDown,
fullWidth,
error,
noErrorHelper = false,
required,
type,
autoFocus,
@ -207,7 +209,9 @@ const TextInputV2Component = (
)}
</StyledTrailingIconContainer>
</StyledInputContainer>
{error && <StyledErrorHelper>{error}</StyledErrorHelper>}
{error && !noErrorHelper && (
<StyledErrorHelper>{error}</StyledErrorHelper>
)}
</StyledContainer>
);
};

View File

@ -47,6 +47,7 @@ const testCases = [
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingWorkspaceActivation, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingProfileCreation, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingSyncEmail, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.OngoingInviteTeam, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Verify, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -58,6 +59,7 @@ const testCases = [
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.SignInUp, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SignInUp, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -69,6 +71,7 @@ const testCases = [
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.Completed, res: true },
{ loc: AppPath.Invite, status: OnboardingStatus.CompletedWithoutSubscription, res: true },
@ -80,6 +83,7 @@ const testCases = [
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.Completed, res: true },
{ loc: AppPath.ResetPassword, status: OnboardingStatus.CompletedWithoutSubscription, res: true },
@ -91,6 +95,7 @@ const testCases = [
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateWorkspace, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -102,6 +107,7 @@ const testCases = [
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.CreateProfile, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -113,9 +119,22 @@ const testCases = [
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SyncEmails, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Incomplete, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Canceled, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Unpaid, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.PastDue, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingUserCreation, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.InviteTeam, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Incomplete, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Canceled, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Unpaid, res: false },
@ -124,6 +143,7 @@ const testCases = [
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.PlanRequired, status: OnboardingStatus.CompletedWithoutSubscription, res: true },
@ -135,6 +155,7 @@ const testCases = [
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.PlanRequiredSuccess, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -146,6 +167,7 @@ const testCases = [
{ loc: AppPath.Index, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.Index, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Index, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -157,6 +179,7 @@ const testCases = [
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.TasksPage, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.TasksPage, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -168,6 +191,7 @@ const testCases = [
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.OpportunitiesPage, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -179,6 +203,7 @@ const testCases = [
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordIndexPage, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -190,6 +215,7 @@ const testCases = [
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.RecordShowPage, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -201,6 +227,7 @@ const testCases = [
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.SettingsCatchAll, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -212,6 +239,7 @@ const testCases = [
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.DevelopersCatchAll, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -223,6 +251,7 @@ const testCases = [
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.Impersonate, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Impersonate, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -234,6 +263,7 @@ const testCases = [
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.Authorize, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.Authorize, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -245,6 +275,7 @@ const testCases = [
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFoundWildcard, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
@ -256,6 +287,7 @@ const testCases = [
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingWorkspaceActivation, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingProfileCreation, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingSyncEmail, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.OngoingInviteTeam, res: true },
{ loc: AppPath.NotFound, status: OnboardingStatus.Completed, res: false },
{ loc: AppPath.NotFound, status: OnboardingStatus.CompletedWithoutSubscription, res: false },
];

View File

@ -28,7 +28,8 @@ export const useShowAuthModal = () => {
OnboardingStatus.OngoingUserCreation === onboardingStatus ||
OnboardingStatus.OngoingProfileCreation === onboardingStatus ||
OnboardingStatus.OngoingWorkspaceActivation === onboardingStatus ||
OnboardingStatus.OngoingSyncEmail === onboardingStatus
OnboardingStatus.OngoingSyncEmail === onboardingStatus ||
OnboardingStatus.OngoingInviteTeam === onboardingStatus
) {
return true;
}

View File

@ -0,0 +1,33 @@
import { useTheme } from '@emotion/react';
import { motion } from 'framer-motion';
type AnimatedTranslationProps = {
children: React.ReactNode;
};
export const AnimatedTranslation = ({ children }: AnimatedTranslationProps) => {
const theme = useTheme();
return (
<motion.div
initial="hidden"
animate="visible"
variants={{
hidden: {
opacity: 0,
y: -20,
},
visible: {
opacity: 1,
y: 0,
transition: {
duration: theme.animation.duration.normal, // Replace this with your theme's duration
ease: 'easeInOut',
},
},
}}
>
{children}
</motion.div>
);
};

View File

@ -8,9 +8,7 @@ export const USER_QUERY_FRAGMENT = gql`
email
canImpersonate
supportUserHash
state {
skipSyncEmailOnboardingStep
}
onboardingStep
workspaceMember {
id
name {

View File

@ -1,17 +1,16 @@
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { Key } from 'ts-key-enum';
import { IconCopy, IconMail, IconSend } from 'twenty-ui';
import { IconMail, IconSend } from 'twenty-ui';
import { z } from 'zod';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Button } from '@/ui/input/button/components/Button';
import { TextInput } from '@/ui/input/components/TextInput';
import { extractEmailsList } from '@/workspace/utils/extractEmailList';
import { sanitizeEmailList } from '@/workspace/utils/sanitizeEmailList';
import { useSendInviteLinkMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
@ -35,7 +34,7 @@ const validationSchema = () =>
if (!value.length) {
return;
}
const emails = extractEmailsList(value);
const emails = sanitizeEmailList(value.split(','));
if (emails.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_string,
@ -69,7 +68,6 @@ type FormInput = {
};
export const WorkspaceInviteTeam = () => {
const theme = useTheme();
const { enqueueSnackBar } = useSnackBar();
const [sendInviteLink] = useSendInviteLinkMutation();
@ -82,14 +80,13 @@ export const WorkspaceInviteTeam = () => {
});
const submit = handleSubmit(async (data) => {
const emailsList = extractEmailsList(data.emails);
const emailsList = sanitizeEmailList(data.emails.split(','));
const result = await sendInviteLink({ variables: { emails: emailsList } });
if (isDefined(result.errors)) {
throw result.errors;
}
enqueueSnackBar('Invite link sent to email addresses', {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
duration: 2000,
});
});

View File

@ -1,28 +0,0 @@
import { extractEmailsList } from '@/workspace/utils/extractEmailList';
describe('extractEmailList', () => {
it('should extract email list', () => {
expect(extractEmailsList('toto@toto.com')).toEqual(['toto@toto.com']);
});
it('should extract email list with multiple emails', () => {
expect(extractEmailsList('toto@toto.com,toto2@toto.com')).toEqual([
'toto@toto.com',
'toto2@toto.com',
]);
});
it('should extract email list with multiple emails and wrong emails', () => {
expect(extractEmailsList('toto@toto.com,toto2@toto.com,toto')).toEqual([
'toto@toto.com',
'toto2@toto.com',
'toto',
]);
});
it('should remove duplicates', () => {
expect(extractEmailsList('toto@toto.com,toto@toto.com')).toEqual([
'toto@toto.com',
]);
});
it('should remove empty emails', () => {
expect(extractEmailsList('toto@toto.com,')).toEqual(['toto@toto.com']);
});
});

View File

@ -0,0 +1,24 @@
import { sanitizeEmailList } from '@/workspace/utils/sanitizeEmailList';
describe('sanitizeEmailList', () => {
it('should do nothing if sanitized email list', () => {
expect(sanitizeEmailList(['toto@toto.com', 'toto2@toto.com'])).toEqual([
'toto@toto.com',
'toto2@toto.com',
]);
});
it('should trim spaces', () => {
expect(sanitizeEmailList([' toto@toto.com ', ' toto2@toto.com'])).toEqual([
'toto@toto.com',
'toto2@toto.com',
]);
});
it('should filter empty emails', () => {
expect(sanitizeEmailList(['toto@toto.com', ''])).toEqual(['toto@toto.com']);
});
it('should remove duplicates', () => {
expect(sanitizeEmailList(['toto@toto.com', 'toto@toto.com'])).toEqual([
'toto@toto.com',
]);
});
});

View File

@ -1,8 +1,7 @@
export const extractEmailsList = (emails: string) => {
export const sanitizeEmailList = (emailList: string[]): string[] => {
return Array.from(
new Set(
emails
.split(',')
emailList
.map((email) => email.trim())
.filter((email) => email.length > 0),
),