4689 multi workspace i should be able to accept an invite if im already logged in (#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
This commit is contained in:
@ -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';
|
||||
@ -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 />} />
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 = () => {
|
||||
@ -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 &&
|
||||
@ -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 (
|
||||
@ -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,
|
||||
|
||||
@ -260,6 +260,7 @@ export type LoginToken = {
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
activateWorkspace: Workspace;
|
||||
addUserToWorkspace: User;
|
||||
authorizeApp: AuthorizeApp;
|
||||
challenge: LoginToken;
|
||||
checkoutSession: SessionEntity;
|
||||
@ -294,6 +295,11 @@ export type MutationActivateWorkspaceArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationAddUserToWorkspaceArgs = {
|
||||
inviteHash: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationAuthorizeAppArgs = {
|
||||
clientId: Scalars['String'];
|
||||
codeChallenge?: InputMaybe<Scalars['String']>;
|
||||
@ -539,6 +545,7 @@ export type RelationConnection = {
|
||||
export type RelationDefinition = {
|
||||
__typename?: 'RelationDefinition';
|
||||
direction: RelationDefinitionType;
|
||||
relationId: Scalars['UUID'];
|
||||
sourceFieldMetadata: Field;
|
||||
sourceObjectMetadata: Object;
|
||||
targetFieldMetadata: Field;
|
||||
@ -578,6 +585,7 @@ export type RemoteTable = {
|
||||
id?: Maybe<Scalars['UUID']>;
|
||||
name: Scalars['String'];
|
||||
schema?: Maybe<Scalars['String']>;
|
||||
schemaPendingUpdates?: Maybe<Array<TableUpdate>>;
|
||||
status: RemoteTableStatus;
|
||||
};
|
||||
|
||||
@ -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'];
|
||||
@ -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;
|
||||
}>;
|
||||
@ -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) {
|
||||
|
||||
@ -6,11 +6,17 @@ 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';
|
||||
@ -18,16 +24,8 @@ 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)};
|
||||
@ -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,
|
||||
@ -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 =
|
||||
@ -143,10 +122,6 @@ export const SignInUpForm = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatedEaseIn>
|
||||
<Logo workspaceLogo={workspace?.logo} />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>{title}</Title>
|
||||
<StyledContentContainer>
|
||||
{authProviders.google && (
|
||||
<>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -80,6 +80,7 @@ export const DefaultLayout = () => {
|
||||
OnboardingStatus.OngoingWorkspaceActivation,
|
||||
].includes(onboardingStatus)) ||
|
||||
isMatchingLocation(AppPath.ResetPassword) ||
|
||||
isMatchingLocation(AppPath.Invite) ||
|
||||
(isMatchingLocation(AppPath.PlanRequired) &&
|
||||
(OnboardingStatus.CompletedWithoutSubscription ||
|
||||
OnboardingStatus.Canceled))
|
||||
|
||||
@ -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);
|
||||
@ -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 };
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const ADD_USER_TO_WORKSPACE = gql`
|
||||
mutation AddUserToWorkspace($inviteHash: String!) {
|
||||
addUserToWorkspace(inviteHash: $inviteHash) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
122
packages/twenty-front/src/pages/auth/Invite.tsx
Normal file
122
packages/twenty-front/src/pages/auth/Invite.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { Logo } from '@/auth/components/Logo';
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
|
||||
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||
import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
|
||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||
import { useAddUserToWorkspaceMutation } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const Invite = () => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
workspace: workspaceFromInviteHash,
|
||||
loading: workspaceFromInviteHashLoading,
|
||||
workspaceInviteHash,
|
||||
} = useWorkspaceFromInviteHash();
|
||||
const { form } = useSignInUpForm();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
const [addUserToWorkspace] = useAddUserToWorkspaceMutation();
|
||||
const { switchWorkspace } = useWorkspaceSwitching();
|
||||
|
||||
const title = useMemo(() => {
|
||||
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
|
||||
}, [workspaceFromInviteHash?.displayName]);
|
||||
|
||||
const handleUserJoinWorkspace = async () => {
|
||||
if (
|
||||
!(isDefined(workspaceInviteHash) && isDefined(workspaceFromInviteHash))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await addUserToWorkspace({
|
||||
variables: {
|
||||
inviteHash: workspaceInviteHash,
|
||||
},
|
||||
});
|
||||
await switchWorkspace(workspaceFromInviteHash.id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isDefined(workspaceFromInviteHash) &&
|
||||
!workspaceFromInviteHashLoading
|
||||
) {
|
||||
enqueueSnackBar('workspace does not exist', {
|
||||
variant: 'error',
|
||||
});
|
||||
if (isDefined(currentWorkspace)) {
|
||||
navigate(AppPath.Index);
|
||||
} else {
|
||||
navigate(AppPath.SignInUp);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isDefined(currentWorkspace) &&
|
||||
currentWorkspace.id === workspaceFromInviteHash?.id
|
||||
) {
|
||||
enqueueSnackBar(
|
||||
`You already belong to ${workspaceFromInviteHash?.displayName} workspace`,
|
||||
{
|
||||
variant: 'info',
|
||||
},
|
||||
);
|
||||
navigate(AppPath.Index);
|
||||
}
|
||||
}, [
|
||||
navigate,
|
||||
enqueueSnackBar,
|
||||
currentWorkspace,
|
||||
workspaceFromInviteHash,
|
||||
workspaceFromInviteHashLoading,
|
||||
]);
|
||||
|
||||
return (
|
||||
!workspaceFromInviteHashLoading && (
|
||||
<>
|
||||
<AnimatedEaseIn>
|
||||
<Logo workspaceLogo={workspaceFromInviteHash?.logo} />
|
||||
</AnimatedEaseIn>
|
||||
<Title animate>{title}</Title>
|
||||
{isDefined(currentWorkspace) && workspaceFromInviteHash ? (
|
||||
<>
|
||||
<StyledContentContainer>
|
||||
<MainButton
|
||||
variant="secondary"
|
||||
title="Continue"
|
||||
type="submit"
|
||||
onClick={handleUserJoinWorkspace}
|
||||
Icon={() => form.formState.isSubmitting && <Loader />}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledContentContainer>
|
||||
<FooterNote>
|
||||
By using Twenty, you agree to the Terms of Service and Privacy
|
||||
Policy.
|
||||
</FooterNote>
|
||||
</>
|
||||
) : (
|
||||
<SignInUpForm />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -1,3 +1,43 @@
|
||||
import { SignInUpForm } from '../../modules/auth/sign-in-up/components/SignInUpForm';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const SignInUp = () => <SignInUpForm />;
|
||||
import { Title } from '@/auth/components/Title';
|
||||
import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm';
|
||||
import {
|
||||
SignInUpMode,
|
||||
SignInUpStep,
|
||||
useSignInUp,
|
||||
} from '@/auth/sign-in-up/hooks/useSignInUp';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const SignInUp = () => {
|
||||
const { form } = useSignInUpForm();
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const { signInUpStep, signInUpMode } = useSignInUp(form);
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (
|
||||
signInUpStep === SignInUpStep.Init ||
|
||||
signInUpStep === SignInUpStep.Email
|
||||
) {
|
||||
return 'Welcome to Twenty';
|
||||
}
|
||||
return signInUpMode === SignInUpMode.SignIn
|
||||
? 'Sign in to Twenty'
|
||||
: 'Sign up to Twenty';
|
||||
}, [signInUpMode, signInUpStep]);
|
||||
|
||||
if (isDefined(currentWorkspace)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title animate>{title}</Title>
|
||||
<SignInUpForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedOnboardingUsersData } from '~/testing/mock-data/users';
|
||||
|
||||
import { SignInUp } from '../SignInUp';
|
||||
|
||||
@ -24,14 +23,25 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
handlers: [
|
||||
graphql.query(getOperationName(GET_CURRENT_USER) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
currentUser: mockedOnboardingUsersData[0],
|
||||
},
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: 'Unauthorized',
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
response: {
|
||||
statusCode: 401,
|
||||
message: 'Unauthorized',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
graphqlMocks.handlers,
|
||||
],
|
||||
},
|
||||
cookie: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -27,7 +27,6 @@ import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-p
|
||||
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
|
||||
import { EmailPasswordResetLinkInput } from 'src/engine/core-modules/auth/dto/email-password-reset-link.input';
|
||||
import { GenerateJwtInput } from 'src/engine/core-modules/auth/dto/generate-jwt.input';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.entity';
|
||||
import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input';
|
||||
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
||||
@ -56,7 +55,6 @@ export class AuthResolver {
|
||||
private authService: AuthService,
|
||||
private tokenService: TokenService,
|
||||
private userService: UserService,
|
||||
private userWorkspaceService: UserWorkspaceService,
|
||||
) {}
|
||||
|
||||
@UseGuards(CaptchaGuard)
|
||||
|
||||
@ -150,26 +150,10 @@ export class SignInUpService {
|
||||
);
|
||||
|
||||
if (existingUser) {
|
||||
const userWorkspaceExists =
|
||||
await this.userWorkspaceService.checkUserWorkspaceExists(
|
||||
existingUser.id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!userWorkspaceExists) {
|
||||
await this.userWorkspaceService.create(existingUser.id, workspace.id);
|
||||
|
||||
await this.userWorkspaceService.createWorkspaceMember(
|
||||
workspace.id,
|
||||
existingUser,
|
||||
);
|
||||
}
|
||||
|
||||
const updatedUser = await this.userRepository.save({
|
||||
id: existingUser.id,
|
||||
defaultWorkspace: workspace,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
|
||||
existingUser,
|
||||
workspace,
|
||||
);
|
||||
|
||||
return Object.assign(existingUser, updatedUser);
|
||||
}
|
||||
|
||||
@ -8,12 +8,13 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestjsQueryGraphQLModule.forFeature({
|
||||
imports: [
|
||||
NestjsQueryTypeOrmModule.forFeature([UserWorkspace], 'core'),
|
||||
NestjsQueryTypeOrmModule.forFeature([User, UserWorkspace], 'core'),
|
||||
TypeORMModule,
|
||||
DataSourceModule,
|
||||
WorkspaceDataSourceModule,
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceInviteHashValidInput } from 'src/engine/core-modules/auth/dto/workspace-invite-hash.input';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Resolver(() => UserWorkspace)
|
||||
export class UserWorkspaceResolver {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => User)
|
||||
async addUserToWorkspace(
|
||||
@AuthUser() user: User,
|
||||
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
|
||||
) {
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
inviteHash: workspaceInviteHashValidInput.inviteHash,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await this.userWorkspaceService.addUserToWorkspace(user, workspace);
|
||||
}
|
||||
}
|
||||
@ -12,11 +12,14 @@ import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/work
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
constructor(
|
||||
@InjectRepository(UserWorkspace, 'core')
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
@ -70,6 +73,25 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
this.eventEmitter.emit('workspaceMember.created', payload);
|
||||
}
|
||||
|
||||
async addUserToWorkspace(user: User, workspace: Workspace) {
|
||||
const userWorkspaceExists = await this.checkUserWorkspaceExists(
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!userWorkspaceExists) {
|
||||
await this.create(user.id, workspace.id);
|
||||
|
||||
await this.createWorkspaceMember(workspace.id, user);
|
||||
}
|
||||
|
||||
return await this.userRepository.save({
|
||||
id: user.id,
|
||||
defaultWorkspace: workspace,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
public async getWorkspaceMemberCount(
|
||||
workspaceId: string,
|
||||
): Promise<number | undefined> {
|
||||
|
||||
@ -15,6 +15,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
||||
import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener';
|
||||
import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserWorkspaceResolver } from 'src/engine/core-modules/user-workspace/user-workspace.resolver';
|
||||
|
||||
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
|
||||
import { Workspace } from './workspace.entity';
|
||||
@ -46,6 +47,7 @@ import { WorkspaceService } from './services/workspace.service';
|
||||
providers: [
|
||||
WorkspaceResolver,
|
||||
WorkspaceService,
|
||||
UserWorkspaceResolver,
|
||||
WorkspaceWorkspaceMemberListener,
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user