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:
martmull
2024-05-20 12:11:38 +02:00
committed by GitHub
parent 1ceeb68da8
commit 88f5eb669e
17 changed files with 340 additions and 101 deletions

View File

@ -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 />} />

View File

@ -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,

View File

@ -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) {

View File

@ -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 && (
<>

View File

@ -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,
};
};

View File

@ -80,6 +80,7 @@ export const DefaultLayout = () => {
OnboardingStatus.OngoingWorkspaceActivation,
].includes(onboardingStatus)) ||
isMatchingLocation(AppPath.ResetPassword) ||
isMatchingLocation(AppPath.Invite) ||
(isMatchingLocation(AppPath.PlanRequired) &&
(OnboardingStatus.CompletedWithoutSubscription ||
OnboardingStatus.Canceled))

View File

@ -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 };

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const ADD_USER_TO_WORKSPACE = gql`
mutation AddUserToWorkspace($inviteHash: String!) {
addUserToWorkspace(inviteHash: $inviteHash) {
id
}
}
`;

View 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 />
)}
</>
)
);
};

View File

@ -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 />
</>
);
};

View File

@ -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: '',
},
};

View File

@ -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)

View File

@ -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);
}

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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> {

View File

@ -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,
],
})