5078 ability to invite team members (#5750)

## Added features
- update team member setting page
- add a section to send invitation by email
- add a new invitation email
- update email font to 'Trebuchet MS' as Google Inter font is not
working, we need to use a web safe font
https://templates.mailchimp.com/design/typography/

## Demo

https://github.com/twentyhq/twenty/assets/29927851/c731d883-1599-4281-87e3-0671f36994ae

## Invitation Email

![image](https://github.com/twentyhq/twenty/assets/29927851/d569fc64-fa0c-4769-a3dd-1193a12b495c)
This commit is contained in:
martmull
2024-06-05 16:35:14 +02:00
committed by GitHub
parent 3c4b497846
commit e9d3ed99ca
28 changed files with 608 additions and 39 deletions

View File

@ -0,0 +1,9 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class SendInviteLink {
@Field(() => Boolean, {
description: 'Boolean that confirms query was dispatched',
})
success: boolean;
}

View File

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

View File

@ -8,6 +8,8 @@ import { User } from 'src/engine/core-modules/user/user.entity';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
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 { WorkspaceService } from './workspace.service';
@ -46,6 +48,14 @@ describe('WorkspaceService', () => {
provide: BillingService,
useValue: {},
},
{
provide: EmailService,
useValue: {},
},
{
provide: EnvironmentService,
useValue: {},
},
],
}).compile();

View File

@ -5,6 +5,8 @@ import assert from 'assert';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
import { SendInviteLinkEmail } from 'twenty-emails';
import { render } from '@react-email/render';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@ -13,6 +15,9 @@ import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/a
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
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';
export class WorkspaceService extends TypeOrmQueryService<Workspace> {
constructor(
@ -25,6 +30,8 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly billingService: BillingService,
private readonly environmentService: EnvironmentService,
private readonly emailService: EmailService,
) {
super(workspaceRepository);
}
@ -92,6 +99,46 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId);
}
async sendInviteLink(
emails: string[],
workspace: Workspace,
sender: User,
): Promise<SendInviteLink> {
if (!workspace?.inviteHash) {
return { success: false };
}
const frontBaseURL = this.environmentService.get('FRONT_BASE_URL');
const inviteLink = `${frontBaseURL}/invite/${workspace.inviteHash}`;
for (const email of emails) {
const emailData = {
link: inviteLink,
workspace: { name: workspace.displayName, logo: workspace.logo },
sender: { email: sender.email, firstName: sender.firstName },
};
const emailTemplate = SendInviteLinkEmail(emailData);
const html = render(emailTemplate, {
pretty: true,
});
const text = render(emailTemplate, {
plainText: true,
});
await this.emailService.send({
from: `${this.environmentService.get(
'EMAIL_FROM_NAME',
)} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`,
to: email,
subject: 'Join your team on Twenty',
text,
html,
});
}
return { success: true };
}
private async reassignOrRemoveUserDefaultWorkspace(
workspaceId: string,
userId: string,

View File

@ -25,6 +25,8 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
import { SendInviteLink } from 'src/engine/core-modules/workspace/dtos/send-invite-link.entity';
import { SendInviteLinkInput } from 'src/engine/core-modules/workspace/dtos/send-invite-link.input';
import { Workspace } from './workspace.entity';
@ -122,4 +124,17 @@ export class WorkspaceResolver {
workspaceId: workspace.id,
});
}
@Mutation(() => SendInviteLink)
async sendInviteLink(
@Args() sendInviteLinkInput: SendInviteLinkInput,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<SendInviteLink> {
return await this.workspaceService.sendInviteLink(
sendInviteLinkInput.emails,
workspace,
user,
);
}
}