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:
@ -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>;
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>({
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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],
|
||||
);
|
||||
};
|
||||
@ -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',
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>;
|
||||
};
|
||||
@ -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 = {};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 },
|
||||
];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -8,9 +8,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
email
|
||||
canImpersonate
|
||||
supportUserHash
|
||||
state {
|
||||
skipSyncEmailOnboardingStep
|
||||
}
|
||||
onboardingStep
|
||||
workspaceMember {
|
||||
id
|
||||
name {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@ -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']);
|
||||
});
|
||||
});
|
||||
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -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),
|
||||
),
|
||||
Reference in New Issue
Block a user