Skip to content

Commit

Permalink
4689 multi workspace i should be able to accept an invite if im alrea…
Browse files Browse the repository at this point in the history
…dy logged in (twentyhq#5454)

- split signInUp to separate Invitation from signInUp
- update redirection logic
- add a resolver for userWorkspace
- add a mutation to add a user to a workspace
- authorize /invite/hash while loggedIn
- add a button to join a workspace

### Base functionnality

https://github.com/twentyhq/twenty/assets/29927851/a1075a4e-a2af-4184-aa3e-e163711277a1

### Error handling

https://github.com/twentyhq/twenty/assets/29927851/1bdd78ce-933a-4860-a87a-3f1f7bda389e
  • Loading branch information
martmull committed May 20, 2024
1 parent 1ceeb68 commit 88f5eb6
Show file tree
Hide file tree
Showing 17 changed files with 340 additions and 101 deletions.
3 changes: 2 additions & 1 deletion packages/twenty-front/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { Authorize } from '~/pages/auth/Authorize';
import { ChooseYourPlan } from '~/pages/auth/ChooseYourPlan';
import { CreateProfile } from '~/pages/auth/CreateProfile';
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
import { Invite } from '~/pages/auth/Invite';
import { PasswordReset } from '~/pages/auth/PasswordReset';
import { PaymentSuccess } from '~/pages/auth/PaymentSuccess';
import { SignInUp } from '~/pages/auth/SignInUp';
Expand Down Expand Up @@ -128,7 +129,7 @@ const createRouter = (isBillingEnabled?: boolean) =>
<Route element={<DefaultLayout />}>
<Route path={AppPath.Verify} element={<VerifyEffect />} />
<Route path={AppPath.SignInUp} element={<SignInUp />} />
<Route path={AppPath.Invite} element={<SignInUp />} />
<Route path={AppPath.Invite} element={<Invite />} />
<Route path={AppPath.ResetPassword} element={<PasswordReset />} />
<Route path={AppPath.CreateWorkspace} element={<CreateWorkspace />} />
<Route path={AppPath.CreateProfile} element={<CreateProfile />} />
Expand Down
34 changes: 4 additions & 30 deletions packages/twenty-front/src/effect-components/PageChangeEffect.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { IconCheckbox } from 'twenty-ui';

Expand All @@ -20,10 +20,8 @@ import { SettingsPath } from '@/types/SettingsPath';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useGetWorkspaceFromInviteHashLazyQuery } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';

import { useIsMatchingLocation } from '../hooks/useIsMatchingLocation';

// TODO: break down into smaller functions and / or hooks
export const PageChangeEffect = () => {
Expand Down Expand Up @@ -70,13 +68,6 @@ export const PageChangeEffect = () => {
isMatchingLocation(AppPath.PlanRequired) ||
isMatchingLocation(AppPath.PlanRequiredSuccess);

const navigateToSignUp = () => {
enqueueSnackBar('workspace does not exist', {
variant: 'error',
});
navigate(AppPath.SignInUp);
};

if (
onboardingStatus === OnboardingStatus.OngoingUserCreation &&
!isMatchingOngoingUserCreationRoute &&
Expand Down Expand Up @@ -115,7 +106,8 @@ export const PageChangeEffect = () => {
navigate(AppPath.CreateProfile);
} else if (
onboardingStatus === OnboardingStatus.Completed &&
isMatchingOnboardingRoute
isMatchingOnboardingRoute &&
!isMatchingLocation(AppPath.Invite)
) {
navigate(AppPath.Index);
} else if (
Expand All @@ -124,24 +116,6 @@ export const PageChangeEffect = () => {
!isMatchingLocation(AppPath.PlanRequired)
) {
navigate(AppPath.Index);
} else if (isMatchingLocation(AppPath.Invite)) {
const inviteHash =
matchPath({ path: '/invite/:workspaceInviteHash' }, location.pathname)
?.params.workspaceInviteHash || '';

workspaceFromInviteHashQuery({
variables: {
inviteHash,
},
onCompleted: (data) => {
if (isUndefinedOrNull(data.findWorkspaceFromInviteHash)) {
navigateToSignUp();
}
},
onError: (_) => {
navigateToSignUp();
},
});
}
}, [
enqueueSnackBar,
Expand Down
56 changes: 56 additions & 0 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export type LoginToken = {
export type Mutation = {
__typename?: 'Mutation';
activateWorkspace: Workspace;
addUserToWorkspace: User;
authorizeApp: AuthorizeApp;
challenge: LoginToken;
checkoutSession: SessionEntity;
Expand Down Expand Up @@ -294,6 +295,11 @@ export type MutationActivateWorkspaceArgs = {
};


export type MutationAddUserToWorkspaceArgs = {
inviteHash: Scalars['String'];
};


export type MutationAuthorizeAppArgs = {
clientId: Scalars['String'];
codeChallenge?: InputMaybe<Scalars['String']>;
Expand Down Expand Up @@ -539,6 +545,7 @@ export type RelationConnection = {
export type RelationDefinition = {
__typename?: 'RelationDefinition';
direction: RelationDefinitionType;
relationId: Scalars['UUID'];
sourceFieldMetadata: Field;
sourceObjectMetadata: Object;
targetFieldMetadata: Field;
Expand Down Expand Up @@ -578,6 +585,7 @@ export type RemoteTable = {
id?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
schema?: Maybe<Scalars['String']>;
schemaPendingUpdates?: Maybe<Array<TableUpdate>>;
status: RemoteTableStatus;
};

Expand Down Expand Up @@ -617,6 +625,14 @@ export type Support = {
supportFrontChatId?: Maybe<Scalars['String']>;
};

/** Schema update on a table */
export enum TableUpdate {
ColumnsAdded = 'COLUMNS_ADDED',
ColumnsDeleted = 'COLUMNS_DELETED',
ColumnsTypeChanged = 'COLUMNS_TYPE_CHANGED',
TableDeleted = 'TABLE_DELETED'
}

export type Telemetry = {
__typename?: 'Telemetry';
anonymizationEnabled: Scalars['Boolean'];
Expand Down Expand Up @@ -1191,6 +1207,13 @@ export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;

export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, currentCacheVersion?: string | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } };

export type AddUserToWorkspaceMutationVariables = Exact<{
inviteHash: Scalars['String'];
}>;


export type AddUserToWorkspaceMutation = { __typename?: 'Mutation', addUserToWorkspace: { __typename?: 'User', id: any } };

export type ActivateWorkspaceMutationVariables = Exact<{
input: ActivateWorkspaceInput;
}>;
Expand Down Expand Up @@ -2456,6 +2479,39 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
export type GetCurrentUserQueryHookResult = ReturnType<typeof useGetCurrentUserQuery>;
export type GetCurrentUserLazyQueryHookResult = ReturnType<typeof useGetCurrentUserLazyQuery>;
export type GetCurrentUserQueryResult = Apollo.QueryResult<GetCurrentUserQuery, GetCurrentUserQueryVariables>;
export const AddUserToWorkspaceDocument = gql`
mutation AddUserToWorkspace($inviteHash: String!) {
addUserToWorkspace(inviteHash: $inviteHash) {
id
}
}
`;
export type AddUserToWorkspaceMutationFn = Apollo.MutationFunction<AddUserToWorkspaceMutation, AddUserToWorkspaceMutationVariables>;

/**
* __useAddUserToWorkspaceMutation__
*
* To run a mutation, you first call `useAddUserToWorkspaceMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useAddUserToWorkspaceMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [addUserToWorkspaceMutation, { data, loading, error }] = useAddUserToWorkspaceMutation({
* variables: {
* inviteHash: // value for 'inviteHash'
* },
* });
*/
export function useAddUserToWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<AddUserToWorkspaceMutation, AddUserToWorkspaceMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<AddUserToWorkspaceMutation, AddUserToWorkspaceMutationVariables>(AddUserToWorkspaceDocument, options);
}
export type AddUserToWorkspaceMutationHookResult = ReturnType<typeof useAddUserToWorkspaceMutation>;
export type AddUserToWorkspaceMutationResult = Apollo.MutationResult<AddUserToWorkspaceMutation>;
export type AddUserToWorkspaceMutationOptions = Apollo.BaseMutationOptions<AddUserToWorkspaceMutation, AddUserToWorkspaceMutationVariables>;
export const ActivateWorkspaceDocument = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,26 @@ import { motion } from 'framer-motion';
import { useRecoilState, useRecoilValue } from 'recoil';
import { IconGoogle, IconMicrosoft } from 'twenty-ui';

import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
import {
SignInUpMode,
SignInUpStep,
useSignInUp,
} from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
import { Loader } from '@/ui/feedback/loader/components/Loader';
import { MainButton } from '@/ui/input/button/components/MainButton';
import { TextInput } from '@/ui/input/components/TextInput';
import { ActionLink } from '@/ui/navigation/link/components/ActionLink';
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
import { isDefined } from '~/utils/isDefined';

import { Logo } from '../../components/Logo';
import { Title } from '../../components/Title';
import { SignInUpMode, SignInUpStep, useSignInUp } from '../hooks/useSignInUp';

import { FooterNote } from './FooterNote';
import { HorizontalSeparator } from './HorizontalSeparator';

const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)};
Expand Down Expand Up @@ -55,14 +53,12 @@ export const SignInUpForm = () => {
);
const [authProviders] = useRecoilState(authProvidersState);
const [showErrors, setShowErrors] = useState(false);
const { handleResetPassword } = useHandleResetPassword();
const workspace = useWorkspaceFromInviteHash();
const { signInWithGoogle } = useSignInWithGoogle();
const { signInWithMicrosoft } = useSignInWithMicrosoft();
const { form } = useSignInUpForm();
const { handleResetPassword } = useHandleResetPassword();

const {
isInviteMode,
signInUpStep,
signInUpMode,
continueWithCredentials,
Expand Down Expand Up @@ -101,23 +97,6 @@ export const SignInUpForm = () => {
return signInUpMode === SignInUpMode.SignIn ? 'Sign in' : 'Sign up';
}, [signInUpMode, signInUpStep]);

const title = useMemo(() => {
if (isInviteMode) {
return `Join ${workspace?.displayName ?? ''} team`;
}

if (
signInUpStep === SignInUpStep.Init ||
signInUpStep === SignInUpStep.Email
) {
return 'Welcome to Twenty';
}

return signInUpMode === SignInUpMode.SignIn
? 'Sign in to Twenty'
: 'Sign up to Twenty';
}, [signInUpMode, workspace?.displayName, isInviteMode, signInUpStep]);

const theme = useTheme();

const shouldWaitForCaptchaToken =
Expand All @@ -143,10 +122,6 @@ export const SignInUpForm = () => {

return (
<>
<AnimatedEaseIn>
<Logo workspaceLogo={workspace?.logo} />
</AnimatedEaseIn>
<Title animate>{title}</Title>
<StyledContentContainer>
{authProviders.google && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';

export const useWorkspaceFromInviteHash = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
const { data: workspaceFromInviteHash } = useGetWorkspaceFromInviteHashQuery({
variables: { inviteHash: workspaceInviteHash || '' },
});
return workspaceFromInviteHash?.findWorkspaceFromInviteHash;
const { data: workspaceFromInviteHash, loading } =
useGetWorkspaceFromInviteHashQuery({
variables: { inviteHash: workspaceInviteHash || '' },
});
return {
workspace: workspaceFromInviteHash?.findWorkspaceFromInviteHash,
workspaceInviteHash,
loading,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const DefaultLayout = () => {
OnboardingStatus.OngoingWorkspaceActivation,
].includes(onboardingStatus)) ||
isMatchingLocation(AppPath.ResetPassword) ||
isMatchingLocation(AppPath.Invite) ||
(isMatchingLocation(AppPath.PlanRequired) &&
(OnboardingStatus.CompletedWithoutSubscription ||
OnboardingStatus.Canceled))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { useNavigate } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil';

import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { AppPath } from '@/types/AppPath';
import { useGenerateJwtMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';

export const useWorkspaceSwitching = () => {
const navigate = useNavigate();
const setTokenPair = useSetRecoilState(tokenPairState);
const [generateJWT] = useGenerateJwtMutation();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
Expand All @@ -30,8 +29,7 @@ export const useWorkspaceSwitching = () => {

const { tokens } = jwt.data.generateJWT;
setTokenPair(tokens);
navigate(`/objects/companies`);
window.location.reload();
window.location.href = AppPath.Index;
};

return { switchWorkspace };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { gql } from '@apollo/client';

export const ADD_USER_TO_WORKSPACE = gql`
mutation AddUserToWorkspace($inviteHash: String!) {
addUserToWorkspace(inviteHash: $inviteHash) {
id
}
}
`;
Loading

0 comments on commit 88f5eb6

Please sign in to comment.