5623 add an inviteteam onboarding step (#5769)

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

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

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

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



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

View File

@ -27,7 +27,7 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/standard-objects/calendar-channel.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { UserStateModule } from 'src/engine/core-modules/user-state/user-state.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { AuthResolver } from './auth.resolver';
@ -64,7 +64,7 @@ const jwtModule = JwtModule.registerAsync({
]),
HttpModule,
UserWorkspaceModule,
UserStateModule,
OnboardingModule,
],
controllers: [
GoogleAuthController,

View File

@ -15,10 +15,10 @@ import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/strategies/googl
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
@Controller('auth/google-apis')
export class GoogleAPIsAuthController {
@ -26,7 +26,7 @@ export class GoogleAPIsAuthController {
private readonly googleAPIsService: GoogleAPIsService,
private readonly tokenService: TokenService,
private readonly environmentService: EnvironmentService,
private readonly userStateService: UserStateService,
private readonly onboardingService: OnboardingService,
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberService: WorkspaceMemberRepository,
) {}
@ -86,7 +86,7 @@ export class GoogleAPIsAuthController {
)?.userId;
if (userId) {
await this.userStateService.skipSyncEmailOnboardingStep(
await this.onboardingService.skipSyncEmailOnboardingStep(
userId,
workspaceId,
);

View File

@ -4,6 +4,7 @@ import {
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
@ -20,6 +21,14 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'keyValuePair', schema: 'core' })
@ObjectType('KeyValuePair')
@Unique('IndexOnKeyUserIdWorkspaceIdUnique', ['key', 'userId', 'workspaceId'])
@Index('IndexOnKeyWorkspaceIdAndNullUserIdUnique', ['key', 'workspaceId'], {
unique: true,
where: '"userId" is NULL',
})
@Index('IndexOnKeyUserIdAndNullWorkspaceIdUnique', ['key', 'userId'], {
unique: true,
where: '"workspaceId" is NULL',
})
export class KeyValuePair {
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')

View File

@ -1,55 +1,70 @@
import { InjectRepository } from '@nestjs/typeorm';
import { BadRequestException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { UserStates } from 'src/engine/core-modules/user-state/enums/user-states.enum';
import { UserStateEmailSyncValues } from 'src/engine/core-modules/user-state/enums/user-state-email-sync-values.enum';
export enum KeyValueTypes {
USER_STATE = 'USER_STATE',
}
type KeyValuePairs = {
[KeyValueTypes.USER_STATE]: {
[UserStates.SYNC_EMAIL_ONBOARDING_STEP]: UserStateEmailSyncValues;
};
};
export class KeyValuePairService<TYPE extends keyof KeyValuePairs> {
export class KeyValuePairService<TYPE> {
constructor(
@InjectRepository(KeyValuePair, 'core')
private readonly keyValuePairRepository: Repository<KeyValuePair>,
) {}
async get<K extends keyof KeyValuePairs[TYPE]>(
userId: string,
workspaceId: string,
key: K,
) {
return await this.keyValuePairRepository.findOne({
where: {
userId,
workspaceId,
key: key as string,
},
});
async get<K extends keyof TYPE>({
userId,
workspaceId,
key,
}: {
userId?: string;
workspaceId?: string;
key: K;
}): Promise<TYPE[K] | undefined> {
return (
await this.keyValuePairRepository.findOne({
where: {
userId,
workspaceId,
key: key as string,
},
})
)?.value as TYPE[K] | undefined;
}
async set<K extends keyof KeyValuePairs[TYPE]>(
userId: string,
workspaceId: string,
key: K,
value: KeyValuePairs[TYPE][K],
) {
await this.keyValuePairRepository.upsert(
{
userId,
workspaceId,
key: key as string,
value: value as string,
},
{ conflictPaths: ['userId', 'workspaceId', 'key'] },
async set<K extends keyof TYPE>({
userId,
workspaceId,
key,
value,
}: {
userId?: string;
workspaceId?: string;
key: K;
value: TYPE[K];
}) {
if (!userId && !workspaceId) {
throw new BadRequestException('userId and workspaceId are undefined');
}
const upsertData = {
userId,
workspaceId,
key: key as string,
value: value as string,
};
const conflictPaths = Object.keys(upsertData).filter(
(key) => key !== 'value' && upsertData[key] !== undefined,
);
const indexPredicate = !userId
? '"userId" is NULL'
: !workspaceId
? '"workspaceId" is NULL'
: undefined;
await this.keyValuePairRepository.upsert(upsertData, {
conflictPaths,
indexPredicate,
});
}
}

View File

@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class SkipSyncEmailOnboardingStep {
export class OnboardingStepSuccess {
@Field(() => Boolean, {
description: 'Boolean that confirms query was dispatched',
})

View File

@ -0,0 +1,4 @@
export enum OnboardingStep {
SYNC_EMAIL = 'SYNC_EMAIL',
INVITE_TEAM = 'INVITE_TEAM',
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboarding.resolver';
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@Module({
imports: [DataSourceModule, UserWorkspaceModule, KeyValuePairModule],
exports: [OnboardingService],
providers: [OnboardingService, OnboardingResolver],
})
export class OnboardingModule {}

View File

@ -2,27 +2,28 @@ import { UseGuards } from '@nestjs/common';
import { Mutation, Resolver } from '@nestjs/graphql';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
import { OnboardingStepSuccess } from 'src/engine/core-modules/onboarding/dtos/onboarding-step-success.dto';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { User } from 'src/engine/core-modules/user/user.entity';
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { SkipSyncEmailOnboardingStep } from 'src/engine/core-modules/user-state/dtos/skip-sync-email.entity-onboarding-step';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
@UseGuards(JwtAuthGuard)
@Resolver(() => UserState)
export class UserStateResolver {
constructor(private readonly userStateService: UserStateService) {}
@Resolver()
export class OnboardingResolver {
constructor(private readonly onboardingService: OnboardingService) {}
@Mutation(() => SkipSyncEmailOnboardingStep)
@Mutation(() => OnboardingStepSuccess)
async skipSyncEmailOnboardingStep(
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<SkipSyncEmailOnboardingStep> {
return await this.userStateService.skipSyncEmailOnboardingStep(
): Promise<OnboardingStepSuccess> {
await this.onboardingService.skipSyncEmailOnboardingStep(
user.id,
workspace.id,
);
return { success: true };
}
}

View File

@ -0,0 +1,98 @@
import { Injectable } from '@nestjs/common';
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
enum OnboardingStepValues {
SKIPPED = 'SKIPPED',
}
enum OnboardingStepKeys {
SYNC_EMAIL_ONBOARDING_STEP = 'SYNC_EMAIL_ONBOARDING_STEP',
INVITE_TEAM_ONBOARDING_STEP = 'INVITE_TEAM_ONBOARDING_STEP',
}
type OnboardingKeyValueType = {
[OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP]: OnboardingStepValues;
[OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP]: OnboardingStepValues;
};
@Injectable()
export class OnboardingService {
constructor(
private readonly userWorkspaceService: UserWorkspaceService,
private readonly keyValuePairService: KeyValuePairService<OnboardingKeyValueType>,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
) {}
private async isSyncEmailOnboardingStep(user: User, workspace: Workspace) {
const syncEmailValue = await this.keyValuePairService.get({
userId: user.id,
workspaceId: workspace.id,
key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP,
});
const isSyncEmailSkipped = syncEmailValue === OnboardingStepValues.SKIPPED;
const connectedAccounts =
await this.connectedAccountRepository.getAllByUserId(
user.id,
workspace.id,
);
return !isSyncEmailSkipped && !connectedAccounts?.length;
}
private async isInviteTeamOnboardingStep(workspace: Workspace) {
const inviteTeamValue = await this.keyValuePairService.get({
workspaceId: workspace.id,
key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP,
});
const isInviteTeamSkipped =
inviteTeamValue === OnboardingStepValues.SKIPPED;
const workspaceMemberCount =
await this.userWorkspaceService.getWorkspaceMemberCount(workspace.id);
return (
!isInviteTeamSkipped &&
(!workspaceMemberCount || workspaceMemberCount <= 1)
);
}
async getOnboardingStep(
user: User,
workspace: Workspace,
): Promise<OnboardingStep | null> {
if (await this.isSyncEmailOnboardingStep(user, workspace)) {
return OnboardingStep.SYNC_EMAIL;
}
if (await this.isInviteTeamOnboardingStep(workspace)) {
return OnboardingStep.INVITE_TEAM;
}
return null;
}
async skipInviteTeamOnboardingStep(workspaceId: string) {
await this.keyValuePairService.set({
workspaceId,
key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP,
value: OnboardingStepValues.SKIPPED,
});
}
async skipSyncEmailOnboardingStep(userId: string, workspaceId: string) {
await this.keyValuePairService.set({
userId,
workspaceId,
key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP,
value: OnboardingStepValues.SKIPPED,
});
}
}

View File

@ -1,5 +0,0 @@
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
export const DEFAULT_USER_STATE: UserState = {
skipSyncEmailOnboardingStep: true,
};

View File

@ -1,7 +0,0 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType('UserState')
export class UserState {
@Field(() => Boolean, { nullable: true })
skipSyncEmailOnboardingStep: boolean | null;
}

View File

@ -1,3 +0,0 @@
export enum UserStateEmailSyncValues {
SKIPPED = 'SKIPPED',
}

View File

@ -1,3 +0,0 @@
export enum UserStates {
SYNC_EMAIL_ONBOARDING_STEP = 'SYNC_EMAIL_ONBOARDING_STEP',
}

View File

@ -1,13 +0,0 @@
import { Module } from '@nestjs/common';
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
import { UserStateResolver } from 'src/engine/core-modules/user-state/user-state.resolver';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
@Module({
imports: [DataSourceModule, KeyValuePairModule],
exports: [UserStateService],
providers: [UserStateService, UserStateResolver],
})
export class UserStateModule {}

View File

@ -1,64 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
import {
KeyValuePairService,
KeyValueTypes,
} from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
import { UserStates } from 'src/engine/core-modules/user-state/enums/user-states.enum';
import { UserStateEmailSyncValues } from 'src/engine/core-modules/user-state/enums/user-state-email-sync-values.enum';
import { SkipSyncEmailOnboardingStep } from 'src/engine/core-modules/user-state/dtos/skip-sync-email.entity-onboarding-step';
@Injectable()
export class UserStateService {
constructor(
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
private readonly keyValuePairService: KeyValuePairService<KeyValueTypes.USER_STATE>,
) {}
async getUserState(user: User, workspace: Workspace): Promise<UserState> {
const connectedAccounts =
await this.connectedAccountRepository.getAllByUserId(
user.id,
workspace.id,
);
if (connectedAccounts?.length) {
return {
skipSyncEmailOnboardingStep: true,
};
}
const skipSyncEmail = await this.keyValuePairService.get(
user.id,
workspace.id,
UserStates.SYNC_EMAIL_ONBOARDING_STEP,
);
return {
skipSyncEmailOnboardingStep:
!!skipSyncEmail &&
skipSyncEmail.value === UserStateEmailSyncValues.SKIPPED,
};
}
async skipSyncEmailOnboardingStep(
userId: string,
workspaceId: string,
): Promise<SkipSyncEmailOnboardingStep> {
await this.keyValuePairService.set(
userId,
workspaceId,
UserStates.SYNC_EMAIL_ONBOARDING_STEP,
UserStateEmailSyncValues.SKIPPED,
);
return { success: true };
}
}

View File

@ -1,4 +1,4 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import {
Entity,
@ -18,7 +18,12 @@ import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-mem
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum';
registerEnumType(OnboardingStep, {
name: 'OnboardingStep',
description: 'Onboarding step',
});
@Entity({ name: 'user', schema: 'core' })
@ObjectType('User')
@ -114,6 +119,6 @@ export class User {
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user)
workspaces: Relation<UserWorkspace[]>;
@Field(() => UserState, { nullable: false })
state: UserState;
@Field(() => OnboardingStep, { nullable: true })
onboardingStep: OnboardingStep;
}

View File

@ -11,7 +11,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { UserStateModule } from 'src/engine/core-modules/user-state/user-state.module';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
@ -28,8 +28,8 @@ import { UserService } from './services/user.service';
}),
DataSourceModule,
FileUploadModule,
UserStateModule,
WorkspaceModule,
OnboardingModule,
],
exports: [UserService],
providers: [UserService, UserResolver, TypeORMService],

View File

@ -27,11 +27,8 @@ import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserState } from 'src/engine/core-modules/user-state/dtos/user-state.dto';
import { UserStateService } from 'src/engine/core-modules/user-state/user-state.service';
import { DEFAULT_USER_STATE } from 'src/engine/core-modules/user-state/constants/default-user-state';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
const getHMACKey = (email?: string, key?: string | null) => {
if (!email || !key) return null;
@ -47,10 +44,10 @@ export class UserResolver {
constructor(
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
private readonly userStateService: UserStateService,
private readonly userService: UserService,
private readonly environmentService: EnvironmentService,
private readonly fileUploadService: FileUploadService,
private readonly onboardingService: OnboardingService,
) {}
@Query(() => User)
@ -119,15 +116,15 @@ export class UserResolver {
return this.userService.deleteUser(userId);
}
@ResolveField(() => UserState)
async state(
@Parent() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<UserState> {
if (!user || !workspace) {
return DEFAULT_USER_STATE;
@ResolveField(() => OnboardingStep)
async onboardingStep(@Parent() user: User): Promise<OnboardingStep | null> {
if (!user) {
return null;
}
return this.userStateService.getUserState(user, workspace);
return this.onboardingService.getOnboardingStep(
user,
user.defaultWorkspace,
);
}
}

View File

@ -1,12 +1,12 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { ArrayNotEmpty, IsArray, IsEmail } from 'class-validator';
import { ArrayUnique, IsArray, IsEmail } from 'class-validator';
@ArgsType()
export class SendInviteLinkInput {
@Field(() => [String])
@IsArray()
@ArrayNotEmpty()
@IsEmail({}, { each: true })
@ArrayUnique()
emails: string[];
}

View File

@ -10,6 +10,7 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { EmailService } from 'src/engine/integrations/email/email.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { WorkspaceService } from './workspace.service';
@ -48,12 +49,16 @@ describe('WorkspaceService', () => {
provide: BillingService,
useValue: {},
},
{
provide: EnvironmentService,
useValue: {},
},
{
provide: EmailService,
useValue: {},
},
{
provide: EnvironmentService,
provide: OnboardingService,
useValue: {},
},
],

View File

@ -18,6 +18,7 @@ import { BillingService } from 'src/engine/core-modules/billing/billing.service'
import { SendInviteLink } from 'src/engine/core-modules/workspace/dtos/send-invite-link.entity';
import { EmailService } from 'src/engine/integrations/email/email.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
export class WorkspaceService extends TypeOrmQueryService<Workspace> {
constructor(
@ -32,6 +33,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private readonly billingService: BillingService,
private readonly environmentService: EnvironmentService,
private readonly emailService: EmailService,
private readonly onboardingService: OnboardingService,
) {
super(workspaceRepository);
}
@ -96,6 +98,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
userId,
workspaceId,
});
await this.onboardingService.skipInviteTeamOnboardingStep(workspaceId);
await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId);
}
@ -107,6 +110,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
if (!workspace?.inviteHash) {
return { success: false };
}
const frontBaseURL = this.environmentService.get('FRONT_BASE_URL');
const inviteLink = `${frontBaseURL}/invite/${workspace.inviteHash}`;
@ -136,6 +140,8 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
});
}
await this.onboardingService.skipInviteTeamOnboardingStep(workspace.id);
return { success: true };
}

View File

@ -16,6 +16,7 @@ import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/worksp
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 { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
import { Workspace } from './workspace.entity';
@ -37,6 +38,7 @@ import { WorkspaceService } from './services/workspace.service';
UserWorkspaceModule,
WorkspaceManagerModule,
DataSourceModule,
OnboardingModule,
TypeORMModule,
],
services: [WorkspaceService],