3889 activate settingsaccountsemailsinboxsettings (#3962)

* update email visibility in settings

* improve styling

* Add contact auto creation toggle to inbox settings

* re
move soonpill

* update Icon

* create job

* Add logic to create contacts and companies for message participants without personId and workspaceMemberId

* add listener

* wip

* wip

* refactoring

* improve structure

* Add isContactAutoCreationEnabled method to MessageChannelService

* wip

* wip

* clean

* add job

* fix bug

* contact creation is working

* wip

* working

* improve code

* improve typing

* resolve conflicts

* fix

* create company repository

* move util

* wip

* fix
This commit is contained in:
bosiraphael
2024-02-14 17:30:17 +01:00
committed by GitHub
parent 0b2ffb0ee6
commit 94ad0e33ec
27 changed files with 607 additions and 164 deletions

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { MessageChannel } from '@/accounts/types/MessageChannel'; import { MessageChannel } from '@/accounts/types/MessageChannel';
import { SettingsAccountsInboxSettingsCardMedia } from '@/settings/accounts/components/SettingsAccountsInboxSettingsCardMedia'; import { SettingsAccountsInboxSettingsCardMedia } from '@/settings/accounts/components/SettingsAccountsInboxSettingsCardMedia';
import { IconSend } from '@/ui/display/icon'; import { IconUser } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title'; import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Toggle } from '@/ui/input/components/Toggle'; import { Toggle } from '@/ui/input/components/Toggle';
import { Card } from '@/ui/layout/card/components/Card'; import { Card } from '@/ui/layout/card/components/Card';
@ -43,7 +43,7 @@ export const SettingsAccountsInboxSettingsContactAutoCreateSection = ({
<Card> <Card>
<StyledCardContent> <StyledCardContent>
<SettingsAccountsInboxSettingsCardMedia> <SettingsAccountsInboxSettingsCardMedia>
<IconSend size={theme.icon.size.sm} stroke={theme.icon.stroke.lg} /> <IconUser size={theme.icon.size.sm} stroke={theme.icon.stroke.lg} />
</SettingsAccountsInboxSettingsCardMedia> </SettingsAccountsInboxSettingsCardMedia>
<StyledTitle>Auto-creation</StyledTitle> <StyledTitle>Auto-creation</StyledTitle>
<Toggle <Toggle

View File

@ -1,5 +1,4 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { SoonPill } from 'tsup.ui.index';
import { SettingsAccountsInboxSettingsCardMedia } from '@/settings/accounts/components/SettingsAccountsInboxSettingsCardMedia'; import { SettingsAccountsInboxSettingsCardMedia } from '@/settings/accounts/components/SettingsAccountsInboxSettingsCardMedia';
import { H2Title } from '@/ui/display/typography/components/H2Title'; import { H2Title } from '@/ui/display/typography/components/H2Title';
@ -23,7 +22,11 @@ const StyledCardContent = styled(CardContent)`
align-items: center; align-items: center;
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(4)}; gap: ${({ theme }) => theme.spacing(4)};
opacity: 0.56; cursor: pointer;
&:hover {
background: ${({ theme }) => theme.background.transparent.lighter};
}
`; `;
const StyledCardMedia = styled(SettingsAccountsInboxSettingsCardMedia)` const StyledCardMedia = styled(SettingsAccountsInboxSettingsCardMedia)`
@ -61,16 +64,6 @@ const StyledRadio = styled(Radio)`
margin-left: auto; margin-left: auto;
`; `;
const StyledSoonPill = styled(SoonPill)`
position: absolute;
right: 0;
top: 0;
`;
const StyledSection = styled(Section)`
position: relative;
`;
const inboxSettingsVisibilityOptions = [ const inboxSettingsVisibilityOptions = [
{ {
title: 'Everything', title: 'Everything',
@ -108,12 +101,11 @@ export const SettingsAccountsInboxSettingsVisibilitySection = ({
onChange, onChange,
value = InboxSettingsVisibilityValue.Everything, value = InboxSettingsVisibilityValue.Everything,
}: SettingsAccountsInboxSettingsVisibilitySectionProps) => ( }: SettingsAccountsInboxSettingsVisibilitySectionProps) => (
<StyledSection> <Section>
<H2Title <H2Title
title="Email visibility" title="Email visibility"
description="Define what will be visible to other users in your workspace" description="Define what will be visible to other users in your workspace"
/> />
<StyledSoonPill />
<Card> <Card>
{inboxSettingsVisibilityOptions.map( {inboxSettingsVisibilityOptions.map(
( (
@ -123,6 +115,7 @@ export const SettingsAccountsInboxSettingsVisibilitySection = ({
<StyledCardContent <StyledCardContent
key={optionValue} key={optionValue}
divider={index < inboxSettingsVisibilityOptions.length - 1} divider={index < inboxSettingsVisibilityOptions.length - 1}
onClick={() => onChange(optionValue)}
> >
<StyledCardMedia> <StyledCardMedia>
<StyledMetadataSkeleton isActive={visibleElements.metadata} /> <StyledMetadataSkeleton isActive={visibleElements.metadata} />
@ -137,11 +130,10 @@ export const SettingsAccountsInboxSettingsVisibilitySection = ({
value={optionValue} value={optionValue}
onCheckedChange={() => onChange(optionValue)} onCheckedChange={() => onChange(optionValue)}
checked={value === optionValue} checked={value === optionValue}
disabled={true}
/> />
</StyledCardContent> </StyledCardContent>
), ),
)} )}
</Card> </Card>
</StyledSection> </Section>
); );

View File

@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { MessageChannel } from '@/accounts/types/MessageChannel'; import { MessageChannel } from '@/accounts/types/MessageChannel';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SettingsAccountsInboxSettingsContactAutoCreateSection } from '@/settings/accounts/components/SettingsAccountsInboxSettingsContactAutoCreationSection';
import { import {
InboxSettingsVisibilityValue, InboxSettingsVisibilityValue,
SettingsAccountsInboxSettingsVisibilitySection, SettingsAccountsInboxSettingsVisibilitySection,
@ -36,6 +37,15 @@ export const SettingsAccountsEmailsInboxSettings = () => {
}); });
}; };
const handleContactAutoCreationToggle = (value: boolean) => {
updateOneRecord({
idToUpdate: messageChannelId,
updateOneRecordInput: {
isContactAutoCreationEnabled: value,
},
});
};
useEffect(() => { useEffect(() => {
if (!loading && !messageChannel) navigate(AppPath.NotFound); if (!loading && !messageChannel) navigate(AppPath.NotFound);
}, [loading, messageChannel, navigate]); }, [loading, messageChannel, navigate]);
@ -61,11 +71,10 @@ export const SettingsAccountsEmailsInboxSettings = () => {
value={messageChannel?.visibility} value={messageChannel?.visibility}
onChange={handleVisibilityChange} onChange={handleVisibilityChange}
/> />
{/* TODO : Add this section when the backend will be ready to auto create contacts */} <SettingsAccountsInboxSettingsContactAutoCreateSection
{/* <SettingsAccountsInboxSettingsContactAutoCreateSection
messageChannel={messageChannel} messageChannel={messageChannel}
onToggle={handleContactAutoCreationToggle} onToggle={handleContactAutoCreationToggle}
/> */} />
</SettingsPageContainer> </SettingsPageContainer>
</SubMenuTopBarContainer> </SubMenuTopBarContainer>
); );

View File

@ -67,8 +67,8 @@ export class GoogleGmailService {
); );
await manager.query( await manager.query(
`INSERT INTO ${dataSourceMetadata.schema}."messageChannel" ("visibility", "handle", "connectedAccountId", "type") VALUES ($1, $2, $3, $4)`, `INSERT INTO ${dataSourceMetadata.schema}."messageChannel" ("visibility", "handle", "connectedAccountId", "type", "isContactAutoCreationEnabled") VALUES ($1, $2, $3, $4, $5)`,
['share_everything', handle, connectedAccountId, 'email'], ['share_everything', handle, connectedAccountId, 'email', true],
); );
}); });

View File

@ -20,6 +20,9 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
import { FetchAllWorkspacesMessagesJob } from 'src/workspace/messaging/commands/crons/fetch-all-workspaces-messages.job'; import { FetchAllWorkspacesMessagesJob } from 'src/workspace/messaging/commands/crons/fetch-all-workspaces-messages.job';
import { ConnectedAccountModule } from 'src/workspace/messaging/repositories/connected-account/connected-account.module'; import { ConnectedAccountModule } from 'src/workspace/messaging/repositories/connected-account/connected-account.module';
import { MatchMessageParticipantJob } from 'src/workspace/messaging/jobs/match-message-participant.job'; import { MatchMessageParticipantJob } from 'src/workspace/messaging/jobs/match-message-participant.job';
import { CreateCompaniesAndContactsAfterSyncJob } from 'src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job';
import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module';
import { MessageChannelModule } from 'src/workspace/messaging/repositories/message-channel/message-channel.module';
import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module'; import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module';
@Module({ @Module({
@ -36,6 +39,8 @@ import { MessageParticipantModule } from 'src/workspace/messaging/repositories/m
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
ConnectedAccountModule, ConnectedAccountModule,
MessageParticipantModule, MessageParticipantModule,
CreateCompaniesAndContactsModule,
MessageChannelModule,
], ],
providers: [ providers: [
{ {
@ -67,6 +72,10 @@ import { MessageParticipantModule } from 'src/workspace/messaging/repositories/m
provide: MatchMessageParticipantJob.name, provide: MatchMessageParticipantJob.name,
useClass: MatchMessageParticipantJob, useClass: MatchMessageParticipantJob,
}, },
{
provide: CreateCompaniesAndContactsAfterSyncJob.name,
useClass: CreateCompaniesAndContactsAfterSyncJob,
},
], ],
}) })
export class JobsModule { export class JobsModule {

View File

@ -0,0 +1,67 @@
import { Injectable, Logger } from '@nestjs/common';
import { CreateContactsAndCompaniesAfterSyncJobData } from 'packages/twenty-server/dist/src/workspace/messaging/jobs/create-contacts-and-companies-after-sync.job';
import { MessageQueueJob } from 'src/integrations/message-queue/interfaces/message-queue-job.interface';
import { CreateCompaniesAndContactsService } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service';
import { MessageChannelService } from 'src/workspace/messaging/repositories/message-channel/message-channel.service';
import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service';
export type CreateCompaniesAndContactsAfterSyncJobData = {
workspaceId: string;
messageChannelId: string;
};
@Injectable()
export class CreateCompaniesAndContactsAfterSyncJob
implements MessageQueueJob<CreateCompaniesAndContactsAfterSyncJobData>
{
private readonly logger = new Logger(
CreateCompaniesAndContactsAfterSyncJob.name,
);
constructor(
private readonly createCompaniesAndContactsService: CreateCompaniesAndContactsService,
private readonly messageChannelService: MessageChannelService,
private readonly messageParticipantService: MessageParticipantService,
) {}
async handle(
data: CreateContactsAndCompaniesAfterSyncJobData,
): Promise<void> {
this.logger.log(
`create contacts and companies after sync for workspace ${data.workspaceId} and messageChannel ${data.messageChannelId}`,
);
const { workspaceId, messageChannelId } = data;
const isContactAutoCreationEnabled =
await this.messageChannelService.getIsContactAutoCreationEnabledByMessageChannelId(
messageChannelId,
workspaceId,
);
if (!isContactAutoCreationEnabled) {
return;
}
const messageParticipantsWithoutPersonIdAndWorkspaceMemberId =
await this.messageParticipantService.getByMessageChannelIdWithoutPersonIdAndWorkspaceMemberId(
messageChannelId,
workspaceId,
);
await this.createCompaniesAndContactsService.createCompaniesAndContacts(
messageParticipantsWithoutPersonIdAndWorkspaceMemberId,
workspaceId,
);
await this.messageParticipantService.updateMessageParticipantsAfterPeopleCreation(
messageParticipantsWithoutPersonIdAndWorkspaceMemberId,
workspaceId,
);
this.logger.log(
`create contacts and companies after sync for workspace ${data.workspaceId} and messageChannel ${data.messageChannelId} done`,
);
}
}

View File

@ -0,0 +1,40 @@
import { Injectable, Inject } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { CreateContactsAndCompaniesAfterSyncJobData } from 'packages/twenty-server/dist/src/workspace/messaging/jobs/create-contacts-and-companies-after-sync.job';
import { ObjectRecordUpdateEvent } from 'src/integrations/event-emitter/types/object-record-update.event';
import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service';
import { MessageChannelObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-channel.object-metadata';
import { objectRecordChangedProperties as objectRecordUpdateEventChangedProperties } from 'src/integrations/event-emitter/utils/object-record-changed-properties.util';
import { CreateCompaniesAndContactsAfterSyncJob } from 'src/workspace/messaging/jobs/create-companies-and-contacts-after-sync.job';
@Injectable()
export class IsContactAutoCreationEnabledListener {
constructor(
@Inject(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@OnEvent('messageChannel.updated')
handleUpdatedEvent(
payload: ObjectRecordUpdateEvent<MessageChannelObjectMetadata>,
) {
if (
objectRecordUpdateEventChangedProperties(
payload.previousRecord,
payload.updatedRecord,
).includes('isContactAutoCreationEnabled') &&
payload.updatedRecord.isContactAutoCreationEnabled
) {
this.messageQueueService.add<CreateContactsAndCompaniesAfterSyncJobData>(
CreateCompaniesAndContactsAfterSyncJob.name,
{
workspaceId: payload.workspaceId,
messageChannelId: payload.updatedRecord.id,
},
);
}
}
}

View File

@ -18,10 +18,14 @@ import { GmailRefreshAccessTokenService } from 'src/workspace/messaging/services
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module'; import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module';
import { MessagingWorkspaceMemberListener } from 'src/workspace/messaging/listeners/messaging-workspace-member.listener'; import { MessagingWorkspaceMemberListener } from 'src/workspace/messaging/listeners/messaging-workspace-member.listener';
import { IsContactAutoCreationEnabledListener } from 'src/workspace/messaging/listeners/is-contact-auto-creation-enabled-listener';
import { MessagingMessageChannelListener } from 'src/workspace/messaging/listeners/messaging-message-channel.listener'; import { MessagingMessageChannelListener } from 'src/workspace/messaging/listeners/messaging-message-channel.listener';
import { MessageService } from 'src/workspace/messaging/repositories/message/message.service'; import { MessageService } from 'src/workspace/messaging/repositories/message/message.service';
import { WorkspaceMemberModule } from 'src/workspace/messaging/repositories/workspace-member/workspace-member.module'; import { WorkspaceMemberModule } from 'src/workspace/messaging/repositories/workspace-member/workspace-member.module';
import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module';
import { CompanyModule } from 'src/workspace/messaging/repositories/company/company.module';
import { PersonModule } from 'src/workspace/messaging/repositories/person/person.module';
@Module({ @Module({
imports: [ imports: [
EnvironmentModule, EnvironmentModule,
@ -32,8 +36,11 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
MessageModule, MessageModule,
MessageThreadModule, MessageThreadModule,
MessageParticipantModule, MessageParticipantModule,
CreateCompaniesAndContactsModule,
WorkspaceMemberModule, WorkspaceMemberModule,
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
CompanyModule,
PersonModule,
], ],
providers: [ providers: [
GmailFullSyncService, GmailFullSyncService,
@ -45,6 +52,7 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity';
CreateCompanyService, CreateCompanyService,
MessagingPersonListener, MessagingPersonListener,
MessagingWorkspaceMemberListener, MessagingWorkspaceMemberListener,
IsContactAutoCreationEnabledListener,
MessagingMessageChannelListener, MessagingMessageChannelListener,
MessageService, MessageService,
], ],

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { CompanyService } from 'src/workspace/messaging/repositories/company/company.service';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
// TODO: Move outside of the messaging module
@Module({
imports: [WorkspaceDataSourceModule],
providers: [CompanyService],
exports: [CompanyService],
})
export class CompanyModule {}

View File

@ -0,0 +1,52 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
// TODO: Move outside of the messaging module
@Injectable()
export class CompanyService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getExistingCompaniesByDomainNames(
domainNames: string[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<{ id: string; domainName: string }[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const existingCompanies =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT id, "domainName" FROM ${dataSourceSchema}.company WHERE "domainName" = ANY($1)`,
[domainNames],
workspaceId,
transactionManager,
);
return existingCompanies;
}
public async createCompany(
id: string,
name: string,
domainName: string,
city: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}.company (id, name, "domainName", address)
VALUES ($1, $2, $3, $4)`,
[id, name, domainName, city],
workspaceId,
transactionManager,
);
}
}

View File

@ -44,6 +44,35 @@ export class MessageChannelService {
return messageChannels[0]; return messageChannels[0];
} }
public async getIsContactAutoCreationEnabledByConnectedAccountIdOrFail(
connectedAccountId: string,
workspaceId: string,
): Promise<boolean> {
const messageChannel = await this.getFirstByConnectedAccountIdOrFail(
connectedAccountId,
workspaceId,
);
return messageChannel.isContactAutoCreationEnabled;
}
public async getIsContactAutoCreationEnabledByMessageChannelId(
messageChannelId: string,
workspaceId: string,
): Promise<boolean> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const messageChannels =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."messageChannel" WHERE "id" = $1 LIMIT 1`,
[messageChannelId],
workspaceId,
);
return messageChannels[0]?.isContactAutoCreationEnabled;
}
public async getByIds( public async getByIds(
ids: string[], ids: string[],
workspaceId: string, workspaceId: string,

View File

@ -1,16 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CreateCompanyModule } from 'src/workspace/messaging/services/create-company/create-company.module'; import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module';
import { CreateContactModule } from 'src/workspace/messaging/services/create-contact/create-contact.module';
import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service'; import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
@Module({ @Module({
imports: [ imports: [WorkspaceDataSourceModule, CreateCompaniesAndContactsModule],
WorkspaceDataSourceModule,
CreateContactModule,
CreateCompanyModule,
],
providers: [MessageParticipantService], providers: [MessageParticipantService],
exports: [MessageParticipantService], exports: [MessageParticipantService],
}) })

View File

@ -5,17 +5,15 @@ import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { MessageParticipantObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-participant.object-metadata'; import { MessageParticipantObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/message-participant.object-metadata';
import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object-record'; import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object-record';
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; import {
import { Participant } from 'src/workspace/messaging/types/gmail-message'; ParticipantWithId,
import { CreateContactService } from 'src/workspace/messaging/services/create-contact/create-contact.service'; Participant,
import { CreateCompanyService } from 'src/workspace/messaging/services/create-company/create-company.service'; } from 'src/workspace/messaging/types/gmail-message';
@Injectable() @Injectable()
export class MessageParticipantService { export class MessageParticipantService {
constructor( constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly createContactService: CreateContactService,
private readonly createCompaniesService: CreateCompanyService,
) {} ) {}
public async getByHandles( public async getByHandles(
@ -68,79 +66,71 @@ export class MessageParticipantService {
); );
} }
public async getByMessageChannelIdWithoutPersonIdAndWorkspaceMemberId(
messageChannelId: string,
workspaceId: string,
transactionManager?: EntityManager,
): Promise<ParticipantWithId[]> {
if (!messageChannelId || !workspaceId) {
return [];
}
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const messageParticipants: ParticipantWithId[] =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT "messageParticipant".id,
"messageParticipant"."role",
"messageParticipant"."handle",
"messageParticipant"."displayName",
"messageParticipant"."personId",
"messageParticipant"."workspaceMemberId",
"messageParticipant"."messageId"
FROM ${dataSourceSchema}."messageParticipant" "messageParticipant"
LEFT JOIN ${dataSourceSchema}."message" ON "messageParticipant"."messageId" = ${dataSourceSchema}."message"."id"
LEFT JOIN ${dataSourceSchema}."messageChannelMessageAssociation" ON ${dataSourceSchema}."messageChannelMessageAssociation"."messageId" = ${dataSourceSchema}."message"."id"
WHERE ${dataSourceSchema}."messageChannelMessageAssociation"."messageChannelId" = $1
AND "messageParticipant"."personId" IS NULL
AND "messageParticipant"."workspaceMemberId" IS NULL`,
[messageChannelId],
workspaceId,
transactionManager,
);
return messageParticipants;
}
public async saveMessageParticipants( public async saveMessageParticipants(
participants: Participant[], participants: Participant[],
messageId: string, messageId: string,
dataSourceMetadata: DataSourceEntity, workspaceId: string,
manager: EntityManager, transactionManager?: EntityManager,
): Promise<void> { ): Promise<void> {
if (!participants) return; if (!participants) return;
const alreadyCreatedContacts = await manager.query( const dataSourceSchema =
`SELECT email FROM ${dataSourceMetadata.schema}."person" WHERE "email" = ANY($1)`, this.workspaceDataSourceService.getSchemaName(workspaceId);
[participants.map((participant) => participant.handle)],
);
const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map(
({ email }) => email,
);
const filteredParticipants = participants.filter(
(participant) =>
!alreadyCreatedContactEmails.includes(participant.handle) &&
participant.handle.includes('@'),
);
const filteredParticipantsWihCompanyDomainNames = filteredParticipants?.map(
(participant) => ({
handle: participant.handle,
displayName: participant.displayName,
companyDomainName: participant.handle
.split('@')?.[1]
.split('.')
.slice(-2)
.join('.')
.toLowerCase(),
}),
);
const domainNamesToCreate = filteredParticipantsWihCompanyDomainNames.map(
(participant) => participant.companyDomainName,
);
const companiesObject = await this.createCompaniesService.createCompanies(
domainNamesToCreate,
dataSourceMetadata,
manager,
);
const contactsToCreate = filteredParticipantsWihCompanyDomainNames.map(
(participant) => ({
handle: participant.handle,
displayName: participant.displayName,
companyId: companiesObject[participant.companyDomainName],
}),
);
await this.createContactService.createContacts(
contactsToCreate,
dataSourceMetadata,
manager,
);
const handles = participants.map((participant) => participant.handle); const handles = participants.map((participant) => participant.handle);
const participantPersonIds = await manager.query( const participantPersonIds =
`SELECT id, email FROM ${dataSourceMetadata.schema}."person" WHERE "email" = ANY($1)`, await this.workspaceDataSourceService.executeRawQuery(
[handles], `SELECT id, email FROM ${dataSourceSchema}."person" WHERE "email" = ANY($1)`,
); [handles],
workspaceId,
transactionManager,
);
const participantWorkspaceMemberIds = await manager.query( const participantWorkspaceMemberIds =
`SELECT "workspaceMember"."id", "connectedAccount"."handle" AS email FROM ${dataSourceMetadata.schema}."workspaceMember" await this.workspaceDataSourceService.executeRawQuery(
JOIN ${dataSourceMetadata.schema}."connectedAccount" ON ${dataSourceMetadata.schema}."workspaceMember"."id" = ${dataSourceMetadata.schema}."connectedAccount"."accountOwnerId" `SELECT "workspaceMember"."id", "connectedAccount"."handle" AS email FROM ${dataSourceSchema}."workspaceMember"
WHERE ${dataSourceMetadata.schema}."connectedAccount"."handle" = ANY($1)`, JOIN ${dataSourceSchema}."connectedAccount" ON ${dataSourceSchema}."workspaceMember"."id" = ${dataSourceSchema}."connectedAccount"."accountOwnerId"
[handles], WHERE ${dataSourceSchema}."connectedAccount"."handle" = ANY($1)`,
); [handles],
workspaceId,
transactionManager,
);
const messageParticipantsToSave = participants.map((participant) => [ const messageParticipantsToSave = participants.map((participant) => [
messageId, messageId,
@ -163,9 +153,54 @@ export class MessageParticipantService {
if (messageParticipantsToSave.length === 0) return; if (messageParticipantsToSave.length === 0) return;
await manager.query( await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceMetadata.schema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString}`, `INSERT INTO ${dataSourceSchema}."messageParticipant" ("messageId", "role", "handle", "displayName", "personId", "workspaceMemberId") VALUES ${valuesString}`,
messageParticipantsToSave.flat(), messageParticipantsToSave.flat(),
workspaceId,
transactionManager,
);
}
public async updateMessageParticipantsAfterPeopleCreation(
participants: ParticipantWithId[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
if (!participants) return;
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const handles = participants.map((participant) => participant.handle);
const participantPersonIds =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT id, email FROM ${dataSourceSchema}."person" WHERE "email" = ANY($1)`,
[handles],
workspaceId,
transactionManager,
);
const messageParticipantsToUpdate = participants.map((participant) => [
participant.id,
participantPersonIds.find(
(e: { id: string; email: string }) => e.email === participant.handle,
)?.id,
]);
if (messageParticipantsToUpdate.length === 0) return;
const valuesString = messageParticipantsToUpdate
.map((_, index) => `($${index * 2 + 1}::uuid, $${index * 2 + 2}::uuid)`)
.join(', ');
await this.workspaceDataSourceService.executeRawQuery(
`UPDATE ${dataSourceSchema}."messageParticipant" AS "messageParticipant" SET "personId" = "data"."personId"
FROM (VALUES ${valuesString}) AS "data"("id", "personId")
WHERE "messageParticipant"."id" = "data"."id"`,
messageParticipantsToUpdate.flat(),
workspaceId,
transactionManager,
); );
} }
} }

View File

@ -1,10 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MessageChannelModule } from 'src/workspace/messaging/repositories/message-channel/message-channel.module';
import { MessageChannelMessageAssociationModule } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-assocation.module'; import { MessageChannelMessageAssociationModule } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-assocation.module';
import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module'; import { MessageParticipantModule } from 'src/workspace/messaging/repositories/message-participant/message-participant.module';
import { MessageThreadModule } from 'src/workspace/messaging/repositories/message-thread/message-thread.module'; import { MessageThreadModule } from 'src/workspace/messaging/repositories/message-thread/message-thread.module';
import { MessageService } from 'src/workspace/messaging/repositories/message/message.service'; import { MessageService } from 'src/workspace/messaging/repositories/message/message.service';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { CreateCompaniesAndContactsModule } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.module';
@Module({ @Module({
imports: [ imports: [
@ -12,6 +14,8 @@ import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/wo
MessageThreadModule, MessageThreadModule,
MessageParticipantModule, MessageParticipantModule,
MessageChannelMessageAssociationModule, MessageChannelMessageAssociationModule,
MessageChannelModule,
CreateCompaniesAndContactsModule,
], ],
providers: [MessageService], providers: [MessageService],
exports: [MessageService], exports: [MessageService],

View File

@ -9,10 +9,12 @@ import { ObjectRecord } from 'src/workspace/workspace-sync-metadata/types/object
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
import { GmailMessage } from 'src/workspace/messaging/types/gmail-message'; import { GmailMessage } from 'src/workspace/messaging/types/gmail-message';
import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata'; import { ConnectedAccountObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/connected-account.object-metadata';
import { MessageChannelService } from 'src/workspace/messaging/repositories/message-channel/message-channel.service';
import { MessageChannelMessageAssociationService } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service'; import { MessageChannelMessageAssociationService } from 'src/workspace/messaging/repositories/message-channel-message-association/message-channel-message-association.service';
import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service'; import { MessageParticipantService } from 'src/workspace/messaging/repositories/message-participant/message-participant.service';
import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service'; import { MessageThreadService } from 'src/workspace/messaging/repositories/message-thread/message-thread.service';
import { isPersonEmail } from 'src/workspace/messaging/utils/is-person-email.util'; import { isPersonEmail } from 'src/workspace/messaging/utils/is-person-email.util';
import { CreateCompaniesAndContactsService } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service';
@Injectable() @Injectable()
export class MessageService { export class MessageService {
constructor( constructor(
@ -20,6 +22,8 @@ export class MessageService {
private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService, private readonly messageChannelMessageAssociationService: MessageChannelMessageAssociationService,
private readonly messageThreadService: MessageThreadService, private readonly messageThreadService: MessageThreadService,
private readonly messageParticipantService: MessageParticipantService, private readonly messageParticipantService: MessageParticipantService,
private readonly messageChannelService: MessageChannelService,
private readonly createCompaniesAndContactsService: CreateCompaniesAndContactsService,
) {} ) {}
public async getFirstByHeaderMessageId( public async getFirstByHeaderMessageId(
@ -193,11 +197,24 @@ export class MessageService {
], ],
); );
const isContactAutoCreationEnabled =
await this.messageChannelService.getIsContactAutoCreationEnabledByConnectedAccountIdOrFail(
connectedAccount.id,
workspaceId,
);
if (isContactAutoCreationEnabled) {
await this.createCompaniesAndContactsService.createCompaniesAndContacts(
message.participants,
workspaceId,
manager,
);
}
await this.messageParticipantService.saveMessageParticipants( await this.messageParticipantService.saveMessageParticipants(
message.participants, message.participants,
newMessageId, newMessageId,
dataSourceMetadata, workspaceId,
manager,
); );
return Promise.resolve(newMessageId); return Promise.resolve(newMessageId);

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { PersonService } from 'src/workspace/messaging/repositories/person/person.service';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
// TODO: Move outside of the messaging module
@Module({
imports: [WorkspaceDataSourceModule],
providers: [PersonService],
exports: [PersonService],
})
export class PersonModule {}

View File

@ -0,0 +1,52 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
// TODO: Move outside of the messaging module
@Injectable()
export class PersonService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
async createPeople(
peopleToCreate: {
id: string;
handle: string;
firstName: string;
lastName: string;
companyId: string;
}[],
workspaceId: string,
transactionManager?: EntityManager,
): Promise<void> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const valuesString = peopleToCreate
.map(
(_, index) =>
`($${index * 5 + 1}, $${index * 5 + 2}, $${index * 5 + 3}, $${
index * 5 + 4
}, $${index * 5 + 5})`,
)
.join(', ');
return await this.workspaceDataSourceService.executeRawQuery(
`INSERT INTO ${dataSourceSchema}.person (id, email, "nameFirstName", "nameLastName", "companyId") VALUES ${valuesString}`,
peopleToCreate
.map((contact) => [
contact.id,
contact.handle,
contact.firstName,
contact.lastName,
contact.companyId,
])
.flat(),
workspaceId,
transactionManager,
);
}
}

View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { CreateCompaniesAndContactsService } from 'src/workspace/messaging/services/create-companies-and-contacts/create-companies-and-contacts.service';
import { CreateCompanyModule } from 'src/workspace/messaging/services/create-company/create-company.module';
import { CreateContactModule } from 'src/workspace/messaging/services/create-contact/create-contact.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
@Module({
imports: [
WorkspaceDataSourceModule,
CreateContactModule,
CreateCompanyModule,
],
providers: [CreateCompaniesAndContactsService],
exports: [CreateCompaniesAndContactsService],
})
export class CreateCompaniesAndContactsModule {}

View File

@ -0,0 +1,76 @@
import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { Participant } from 'src/workspace/messaging/types/gmail-message';
import { getDomainNameFromHandle } from 'src/workspace/messaging/utils/get-domain-name-from-handle.util';
import { CreateCompanyService } from 'src/workspace/messaging/services/create-company/create-company.service';
import { CreateContactService } from 'src/workspace/messaging/services/create-contact/create-contact.service';
@Injectable()
export class CreateCompaniesAndContactsService {
constructor(
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
private readonly createContactService: CreateContactService,
private readonly createCompaniesService: CreateCompanyService,
) {}
async createCompaniesAndContacts(
participants: Participant[],
workspaceId: string,
transactionManager?: EntityManager,
) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const alreadyCreatedContacts =
await this.workspaceDataSourceService.executeRawQuery(
`SELECT email FROM ${dataSourceSchema}."person" WHERE "email" = ANY($1)`,
[participants.map((participant) => participant.handle)],
workspaceId,
transactionManager,
);
const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map(
({ email }) => email,
);
const filteredParticipants = participants.filter(
(participant) =>
!alreadyCreatedContactEmails.includes(participant.handle) &&
participant.handle.includes('@'),
);
const filteredParticipantsWithCompanyDomainNames =
filteredParticipants?.map((participant) => ({
handle: participant.handle,
displayName: participant.displayName,
companyDomainName: getDomainNameFromHandle(participant.handle),
}));
const domainNamesToCreate = filteredParticipantsWithCompanyDomainNames.map(
(participant) => participant.companyDomainName,
);
const companiesObject = await this.createCompaniesService.createCompanies(
domainNamesToCreate,
workspaceId,
transactionManager,
);
const contactsToCreate = filteredParticipantsWithCompanyDomainNames.map(
(participant) => ({
handle: participant.handle,
displayName: participant.displayName,
companyId: companiesObject[participant.companyDomainName],
}),
);
await this.createContactService.createContacts(
contactsToCreate,
workspaceId,
transactionManager,
);
}
}

View File

@ -1,9 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { CreateCompanyService } from 'src/workspace/messaging/services/create-company/create-company.service'; import { CreateCompanyService } from 'src/workspace/messaging/services/create-company/create-company.service';
import { CompanyModule } from 'src/workspace/messaging/repositories/company/company.module';
@Module({ @Module({
imports: [], imports: [WorkspaceDataSourceModule, CompanyModule],
providers: [CreateCompanyService], providers: [CreateCompanyService],
exports: [CreateCompanyService], exports: [CreateCompanyService],
}) })

View File

@ -1,16 +1,16 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm'; import { EntityManager } from 'typeorm';
import axios, { AxiosInstance } from 'axios';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import axios, { AxiosInstance } from 'axios';
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; import { CompanyService } from 'src/workspace/messaging/repositories/company/company.service';
import { capitalize } from 'src/utils/capitalize'; import { capitalize } from 'src/utils/capitalize';
@Injectable() @Injectable()
export class CreateCompanyService { export class CreateCompanyService {
private readonly httpService: AxiosInstance; private readonly httpService: AxiosInstance;
constructor() { constructor(private readonly companyService: CompanyService) {
this.httpService = axios.create({ this.httpService = axios.create({
baseURL: 'https://companies.twenty.com', baseURL: 'https://companies.twenty.com',
}); });
@ -18,17 +18,19 @@ export class CreateCompanyService {
async createCompanies( async createCompanies(
domainNames: string[], domainNames: string[],
dataSourceMetadata: DataSourceEntity, workspaceId: string,
manager: EntityManager, transactionManager?: EntityManager,
): Promise<{ ): Promise<{
[domainName: string]: string; [domainName: string]: string;
}> { }> {
const uniqueDomainNames = [...new Set(domainNames)]; const uniqueDomainNames = [...new Set(domainNames)];
const existingCompanies = await manager.query( const existingCompanies =
`SELECT id, "domainName" FROM ${dataSourceMetadata.schema}.company WHERE "domainName" = ANY($1)`, await this.companyService.getExistingCompaniesByDomainNames(
[uniqueDomainNames], uniqueDomainNames,
); workspaceId,
transactionManager,
);
const companiesObject = existingCompanies.reduce( const companiesObject = existingCompanies.reduce(
( (
@ -57,8 +59,8 @@ export class CreateCompanyService {
for (const domainName of filteredDomainNames) { for (const domainName of filteredDomainNames) {
companiesObject[domainName] = await this.createCompany( companiesObject[domainName] = await this.createCompany(
domainName, domainName,
dataSourceMetadata, workspaceId,
manager, transactionManager,
); );
} }
@ -67,17 +69,20 @@ export class CreateCompanyService {
async createCompany( async createCompany(
domainName: string, domainName: string,
dataSourceMetadata: DataSourceEntity, workspaceId: string,
manager: EntityManager, transactionManager?: EntityManager,
): Promise<string> { ): Promise<string> {
const companyId = v4(); const companyId = v4();
const { name, city } = await this.getCompanyInfoFromDomainName(domainName); const { name, city } = await this.getCompanyInfoFromDomainName(domainName);
await manager.query( this.companyService.createCompany(
`INSERT INTO ${dataSourceMetadata.schema}.company (id, name, "domainName", address) companyId,
VALUES ($1, $2, $3, $4)`, name,
[companyId, name, domainName, city], domainName,
city,
workspaceId,
transactionManager,
); );
return companyId; return companyId;

View File

@ -1,9 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { CreateContactService } from 'src/workspace/messaging/services/create-contact/create-contact.service'; import { CreateContactService } from 'src/workspace/messaging/services/create-contact/create-contact.service';
import { PersonModule } from 'src/workspace/messaging/repositories/person/person.module';
@Module({ @Module({
imports: [], imports: [WorkspaceDataSourceModule, PersonModule],
providers: [CreateContactService], providers: [CreateContactService],
exports: [CreateContactService], exports: [CreateContactService],
}) })

View File

@ -3,8 +3,8 @@ import { Injectable } from '@nestjs/common';
import { EntityManager } from 'typeorm'; import { EntityManager } from 'typeorm';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity'; import { PersonService } from 'src/workspace/messaging/repositories/person/person.service';
import { capitalize } from 'src/utils/capitalize'; import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/workspace/messaging/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
type ContactToCreate = { type ContactToCreate = {
handle: string; handle: string;
@ -22,67 +22,42 @@ type FormattedContactToCreate = {
@Injectable() @Injectable()
export class CreateContactService { export class CreateContactService {
constructor() {} constructor(private readonly personService: PersonService) {}
formatContacts( public formatContacts(
contactsToCreate: ContactToCreate[], contactsToCreate: ContactToCreate[],
): FormattedContactToCreate[] { ): FormattedContactToCreate[] {
return contactsToCreate.map((contact) => { return contactsToCreate.map((contact) => {
const id = v4();
const { handle, displayName, companyId } = contact; const { handle, displayName, companyId } = contact;
const contactFirstName = displayName.split(' ')[0]; const { firstName, lastName } =
const contactLastName = displayName.split(' ')[1]; getFirstNameAndLastNameFromHandleAndDisplayName(handle, displayName);
const contactFullNameFromHandle = handle.split('@')[0];
const contactFirstNameFromHandle =
contactFullNameFromHandle.split('.')[0];
const contactLastNameFromHandle = contactFullNameFromHandle.split('.')[1];
const id = v4();
return { return {
id, id,
handle, handle,
firstName: capitalize( firstName,
contactFirstName || contactFirstNameFromHandle || '', lastName,
),
lastName: capitalize(
contactLastName || contactLastNameFromHandle || '',
),
companyId, companyId,
}; };
}); });
} }
async createContacts( public async createContacts(
contactsToCreate: ContactToCreate[], contactsToCreate: ContactToCreate[],
dataSourceMetadata: DataSourceEntity, workspaceId: string,
manager: EntityManager, transactionManager?: EntityManager,
): Promise<void> { ): Promise<void> {
if (contactsToCreate.length === 0) return; if (contactsToCreate.length === 0) return;
const formattedContacts = this.formatContacts(contactsToCreate); const formattedContacts = this.formatContacts(contactsToCreate);
const valuesString = formattedContacts await this.personService.createPeople(
.map( formattedContacts,
(_, index) => workspaceId,
`($${index * 5 + 1}, $${index * 5 + 2}, $${index * 5 + 3}, $${ transactionManager,
index * 5 + 4
}, $${index * 5 + 5})`,
)
.join(', ');
await manager.query(
`INSERT INTO ${dataSourceMetadata.schema}.person (id, email, "nameFirstName", "nameLastName", "companyId") VALUES ${valuesString}`,
formattedContacts
.map((contact) => [
contact.id,
contact.handle,
contact.firstName,
contact.lastName,
contact.companyId,
])
.flat(),
); );
} }
} }

View File

@ -20,3 +20,7 @@ export type Participant = {
handle: string; handle: string;
displayName: string; displayName: string;
}; };
export type ParticipantWithId = Participant & {
id: string;
};

View File

@ -0,0 +1,3 @@
export function getDomainNameFromHandle(handle: string): string {
return handle.split('@')?.[1].split('.').slice(-2).join('.').toLowerCase();
}

View File

@ -0,0 +1,18 @@
import { capitalize } from 'src/utils/capitalize';
export function getFirstNameAndLastNameFromHandleAndDisplayName(
handle: string,
displayName: string,
): { firstName: string; lastName: string } {
const firstName = displayName.split(' ')[0];
const lastName = displayName.split(' ')[1];
const contactFullNameFromHandle = handle.split('@')[0];
const firstNameFromHandle = contactFullNameFromHandle.split('.')[0];
const lastNameFromHandle = contactFullNameFromHandle.split('.')[1];
return {
firstName: capitalize(firstName || firstNameFromHandle || ''),
lastName: capitalize(lastName || lastNameFromHandle || ''),
};
}

View File

@ -71,6 +71,14 @@ export class MessageChannelObjectMetadata extends BaseObjectMetadata {
}) })
type: string; type: string;
@FieldMetadata({
type: FieldMetadataType.BOOLEAN,
label: 'Is Contact Auto Creation Enabled',
description: 'Is Contact Auto Creation Enabled',
icon: 'IconUserCircle',
})
isContactAutoCreationEnabled: boolean;
@FieldMetadata({ @FieldMetadata({
type: FieldMetadataType.RELATION, type: FieldMetadataType.RELATION,
label: 'Message Channel Association', label: 'Message Channel Association',