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:
@ -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,
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
})
|
||||
@ -0,0 +1,4 @@
|
||||
export enum OnboardingStep {
|
||||
SYNC_EMAIL = 'SYNC_EMAIL',
|
||||
INVITE_TEAM = 'INVITE_TEAM',
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('UserState')
|
||||
export class UserState {
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
skipSyncEmailOnboardingStep: boolean | null;
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export enum UserStateEmailSyncValues {
|
||||
SKIPPED = 'SKIPPED',
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export enum UserStates {
|
||||
SYNC_EMAIL_ONBOARDING_STEP = 'SYNC_EMAIL_ONBOARDING_STEP',
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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: {},
|
||||
},
|
||||
],
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@ -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],
|
||||
|
||||
Reference in New Issue
Block a user