feat(messaging): add workspace toggle to sync internal emails (#20457)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -932,6 +932,7 @@ type Workspace {
|
||||
isMicrosoftAuthEnabled: Boolean!
|
||||
isMicrosoftAuthBypassEnabled: Boolean!
|
||||
isCustomDomainEnabled: Boolean!
|
||||
isInternalMessagesImportEnabled: Boolean!
|
||||
editableProfileFields: [String!]
|
||||
defaultRole: Role
|
||||
fastModel: String!
|
||||
@@ -4318,6 +4319,7 @@ input UpdateWorkspaceInput {
|
||||
editableProfileFields: [String!]
|
||||
enabledAiModelIds: [String!]
|
||||
useRecommendedModels: Boolean
|
||||
isInternalMessagesImportEnabled: Boolean
|
||||
}
|
||||
|
||||
input WorkspaceMigrationInput {
|
||||
|
||||
@@ -652,6 +652,7 @@ export interface Workspace {
|
||||
isMicrosoftAuthEnabled: Scalars['Boolean']
|
||||
isMicrosoftAuthBypassEnabled: Scalars['Boolean']
|
||||
isCustomDomainEnabled: Scalars['Boolean']
|
||||
isInternalMessagesImportEnabled: Scalars['Boolean']
|
||||
editableProfileFields?: Scalars['String'][]
|
||||
defaultRole?: Role
|
||||
fastModel: Scalars['String']
|
||||
@@ -3566,6 +3567,7 @@ export interface WorkspaceGenqlSelection{
|
||||
isMicrosoftAuthEnabled?: boolean | number
|
||||
isMicrosoftAuthBypassEnabled?: boolean | number
|
||||
isCustomDomainEnabled?: boolean | number
|
||||
isInternalMessagesImportEnabled?: boolean | number
|
||||
editableProfileFields?: boolean | number
|
||||
defaultRole?: RoleGenqlSelection
|
||||
fastModel?: boolean | number
|
||||
@@ -6269,7 +6271,7 @@ export interface UpdateWorkspaceMemberSettingsInput {workspaceMemberId: Scalars[
|
||||
|
||||
export interface ActivateWorkspaceInput {displayName?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpdateWorkspaceInput {subdomain?: (Scalars['String'] | null),customDomain?: (Scalars['String'] | null),displayName?: (Scalars['String'] | null),logo?: (Scalars['String'] | null),inviteHash?: (Scalars['String'] | null),isPublicInviteLinkEnabled?: (Scalars['Boolean'] | null),allowImpersonation?: (Scalars['Boolean'] | null),isGoogleAuthEnabled?: (Scalars['Boolean'] | null),isMicrosoftAuthEnabled?: (Scalars['Boolean'] | null),isPasswordAuthEnabled?: (Scalars['Boolean'] | null),isGoogleAuthBypassEnabled?: (Scalars['Boolean'] | null),isMicrosoftAuthBypassEnabled?: (Scalars['Boolean'] | null),isPasswordAuthBypassEnabled?: (Scalars['Boolean'] | null),defaultRoleId?: (Scalars['UUID'] | null),isTwoFactorAuthenticationEnforced?: (Scalars['Boolean'] | null),trashRetentionDays?: (Scalars['Float'] | null),eventLogRetentionDays?: (Scalars['Float'] | null),fastModel?: (Scalars['String'] | null),smartModel?: (Scalars['String'] | null),aiAdditionalInstructions?: (Scalars['String'] | null),editableProfileFields?: (Scalars['String'][] | null),enabledAiModelIds?: (Scalars['String'][] | null),useRecommendedModels?: (Scalars['Boolean'] | null)}
|
||||
export interface UpdateWorkspaceInput {subdomain?: (Scalars['String'] | null),customDomain?: (Scalars['String'] | null),displayName?: (Scalars['String'] | null),logo?: (Scalars['String'] | null),inviteHash?: (Scalars['String'] | null),isPublicInviteLinkEnabled?: (Scalars['Boolean'] | null),allowImpersonation?: (Scalars['Boolean'] | null),isGoogleAuthEnabled?: (Scalars['Boolean'] | null),isMicrosoftAuthEnabled?: (Scalars['Boolean'] | null),isPasswordAuthEnabled?: (Scalars['Boolean'] | null),isGoogleAuthBypassEnabled?: (Scalars['Boolean'] | null),isMicrosoftAuthBypassEnabled?: (Scalars['Boolean'] | null),isPasswordAuthBypassEnabled?: (Scalars['Boolean'] | null),defaultRoleId?: (Scalars['UUID'] | null),isTwoFactorAuthenticationEnforced?: (Scalars['Boolean'] | null),trashRetentionDays?: (Scalars['Float'] | null),eventLogRetentionDays?: (Scalars['Float'] | null),fastModel?: (Scalars['String'] | null),smartModel?: (Scalars['String'] | null),aiAdditionalInstructions?: (Scalars['String'] | null),editableProfileFields?: (Scalars['String'][] | null),enabledAiModelIds?: (Scalars['String'][] | null),useRecommendedModels?: (Scalars['Boolean'] | null),isInternalMessagesImportEnabled?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface WorkspaceMigrationInput {actions: WorkspaceMigrationDeleteActionInput[]}
|
||||
|
||||
|
||||
@@ -1835,6 +1835,9 @@ export default {
|
||||
"isCustomDomainEnabled": [
|
||||
6
|
||||
],
|
||||
"isInternalMessagesImportEnabled": [
|
||||
6
|
||||
],
|
||||
"editableProfileFields": [
|
||||
1
|
||||
],
|
||||
@@ -10952,6 +10955,9 @@ export default {
|
||||
"useRecommendedModels": [
|
||||
6
|
||||
],
|
||||
"isInternalMessagesImportEnabled": [
|
||||
6
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -85,6 +85,7 @@ const mockWorkspace = {
|
||||
routerModel: 'auto',
|
||||
enabledAiModelIds: [],
|
||||
useRecommendedModels: true,
|
||||
isInternalMessagesImportEnabled: false,
|
||||
workspaceCustomApplication: CUSTOM_WORKSPACE_APPLICATION_MOCK,
|
||||
workspaceCustomApplicationId: CUSTOM_WORKSPACE_APPLICATION_MOCK.id,
|
||||
installedApplications: [],
|
||||
|
||||
@@ -42,6 +42,7 @@ export type CurrentWorkspace = Pick<
|
||||
| 'editableProfileFields'
|
||||
| 'enabledAiModelIds'
|
||||
| 'useRecommendedModels'
|
||||
| 'isInternalMessagesImportEnabled'
|
||||
> & {
|
||||
defaultRole?: Omit<Role, 'workspaceMembers' | 'agents' | 'apiKeys'> | null;
|
||||
workspaceCustomApplication: Pick<Application, 'id'> | null;
|
||||
|
||||
+1
@@ -75,6 +75,7 @@ describe('useColumnDefinitionsFromObjectMetadata', () => {
|
||||
smartModel: AUTO_SELECT_SMART_MODEL_ID,
|
||||
enabledAiModelIds: [],
|
||||
useRecommendedModels: true,
|
||||
isInternalMessagesImportEnabled: false,
|
||||
});
|
||||
|
||||
const companyObjectMetadata = getTestEnrichedObjectMetadataItemsMock().find(
|
||||
|
||||
+10
-1
@@ -1,4 +1,5 @@
|
||||
import { type BlocklistItem } from '@/accounts/types/BlocklistItem';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
@@ -16,9 +17,13 @@ export const SettingsAccountsBlocklistSection = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
||||
const currentWorkspaceMemberId = currentWorkspaceMember?.id ?? '';
|
||||
|
||||
const isInternalMessagesImportEnabled =
|
||||
currentWorkspace?.isInternalMessagesImportEnabled ?? false;
|
||||
|
||||
const { records: blocklist } = useFindManyRecords<BlocklistItem>({
|
||||
objectNameSingular: CoreObjectNameSingular.Blocklist,
|
||||
filter: {
|
||||
@@ -53,7 +58,11 @@ export const SettingsAccountsBlocklistSection = () => {
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Blocklist`}
|
||||
description={t`Exclude the following people and domains from my email sync. Internal conversations will not be imported`}
|
||||
description={
|
||||
isInternalMessagesImportEnabled
|
||||
? t`Exclude the following people and domains from my email sync.`
|
||||
: t`Exclude the following people and domains from my email sync. Internal conversations will not be imported`
|
||||
}
|
||||
/>
|
||||
<SettingsAccountsBlocklistInput
|
||||
blockedEmailOrDomainList={blocklist.map((item) => item.handle)}
|
||||
|
||||
@@ -104,6 +104,7 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
trashRetentionDays
|
||||
eventLogRetentionDays
|
||||
editableProfileFields
|
||||
isInternalMessagesImportEnabled
|
||||
}
|
||||
availableWorkspaces {
|
||||
...AvailableWorkspacesFragment
|
||||
|
||||
@@ -16,6 +16,7 @@ export const UPDATE_WORKSPACE = gql`
|
||||
isMicrosoftAuthEnabled
|
||||
isPasswordAuthEnabled
|
||||
isTwoFactorAuthenticationEnforced
|
||||
isInternalMessagesImportEnabled
|
||||
defaultRole {
|
||||
...RoleFragment
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { SettingsSecurityAuthBypassOptionsList } from '@/settings/security/compo
|
||||
import { SettingsSecurityAuthProvidersOptionsList } from '@/settings/security/components/SettingsSecurityAuthProvidersOptionsList';
|
||||
import { SettingsSecurityEditableProfileFields } from '@/settings/security/components/SettingsSecurityEditableProfileFields';
|
||||
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { ToggleImpersonate } from '@/settings/workspace/components/ToggleImpersonate';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
IconClockHour8,
|
||||
IconHistory,
|
||||
IconLock,
|
||||
IconMail,
|
||||
IconTrash,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
@@ -131,6 +133,33 @@ export const SettingsSecurity = () => {
|
||||
saveTrashRetention(value);
|
||||
};
|
||||
|
||||
const handleSyncInternalEmailsChange = (value: boolean) => {
|
||||
if (!currentWorkspace) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === currentWorkspace.isInternalMessagesImportEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentWorkspace({
|
||||
...currentWorkspace,
|
||||
isInternalMessagesImportEnabled: value,
|
||||
});
|
||||
|
||||
updateWorkspace({
|
||||
variables: {
|
||||
input: {
|
||||
isInternalMessagesImportEnabled: value,
|
||||
},
|
||||
},
|
||||
}).catch((err) => {
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: CombinedGraphQLErrors.is(err) ? err : undefined,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleEventLogRetentionDaysChange = (value: number) => {
|
||||
if (!currentWorkspace) {
|
||||
return;
|
||||
@@ -316,6 +345,17 @@ export const SettingsSecurity = () => {
|
||||
minValue={0}
|
||||
showButtons={false}
|
||||
/>
|
||||
<Separator />
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconMail}
|
||||
title={t`Sync Internal Emails`}
|
||||
description={t`Include emails where all participants share the same domain.`}
|
||||
checked={
|
||||
currentWorkspace?.isInternalMessagesImportEnabled ?? false
|
||||
}
|
||||
onChange={handleSyncInternalEmailsChange}
|
||||
advancedMode
|
||||
/>
|
||||
</Card>
|
||||
</Section>
|
||||
</StyledMainContent>
|
||||
|
||||
@@ -94,6 +94,7 @@ export const mockCurrentWorkspace = {
|
||||
routerModel: 'auto',
|
||||
enabledAiModelIds: [],
|
||||
useRecommendedModels: true,
|
||||
isInternalMessagesImportEnabled: false,
|
||||
currentBillingSubscription: {
|
||||
__typename: 'BillingSubscription',
|
||||
id: '7efbc3f7-6e5e-4128-957e-8d86808cdf6a',
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||
|
||||
@RegisteredInstanceCommand('2.5.0', 1778525104406)
|
||||
export class AddIsInternalMessagesImportEnabledFastInstanceCommand
|
||||
implements FastInstanceCommand
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."workspace" ADD COLUMN IF NOT EXISTS "isInternalMessagesImportEnabled" boolean NOT NULL DEFAULT false',
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
'ALTER TABLE "core"."workspace" DROP COLUMN IF EXISTS "isInternalMessagesImportEnabled"',
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -18,7 +18,6 @@ import { DropWorkspaceVersionColumnFastInstanceCommand } from 'src/database/comm
|
||||
import { AddIsPreInstalledToApplicationRegistrationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-1/2-1-instance-command-fast-1776886452831-add-is-pre-installed-to-application-registration';
|
||||
import { AddProviderExecutedToAgentMessagePartFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-1/2-1-instance-command-fast-1777012800000-add-provider-executed-to-agent-message-part';
|
||||
import { BackfillPageLayoutWidgetPositionSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-1/2-1-instance-command-slow-1795000002000-backfill-page-layout-widget-position';
|
||||
import { AddMetadataToBillingPriceFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1777100000000-add-metadata-to-billing-price';
|
||||
import { AddCacheTokensToAgentChatThreadFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777455269302-add-cache-tokens-to-agent-chat-thread';
|
||||
import { AddLogoToApplicationFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-2/2-2-instance-command-fast-1777539664664-add-logo-to-application';
|
||||
import { AddUpgradeMigrationWorkspaceIdIndexFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1777308014234-add-upgrade-migration-workspace-id-index';
|
||||
@@ -29,8 +28,10 @@ import { TransformApplicationVariableToSyncableEntityFastInstanceCommand } from
|
||||
import { AddToolAndWorkflowActionTriggerSettingsFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-fast-1797000001000-add-tool-and-workflow-action-trigger-settings';
|
||||
import { BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1777966965588-backfill-application-variable-universal-identifier';
|
||||
import { MigrateToolTriggerSettingsSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-3/2-3-instance-command-slow-1797000002000-migrate-tool-trigger-settings';
|
||||
import { AddMetadataToBillingPriceFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1777100000000-add-metadata-to-billing-price';
|
||||
import { AddEmailGroupChannelTypeFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1778256809018-add-email-group-channel-type';
|
||||
import { AddApplicationIdToPublicDomainFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-4/2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain';
|
||||
import { AddIsInternalMessagesImportEnabledFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-fast-1778525104406-add-is-internal-messages-import-enabled';
|
||||
import { EncryptConnectedAccountTokensSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-5/2-5-instance-command-slow-1798000004000-encrypt-connected-account-tokens';
|
||||
|
||||
export const INSTANCE_COMMANDS = [
|
||||
@@ -65,5 +66,6 @@ export const INSTANCE_COMMANDS = [
|
||||
BackfillApplicationVariableUniversalIdentifierSlowInstanceCommand,
|
||||
AddEmailGroupChannelTypeFastInstanceCommand,
|
||||
AddApplicationIdToPublicDomainFastInstanceCommand,
|
||||
AddIsInternalMessagesImportEnabledFastInstanceCommand,
|
||||
EncryptConnectedAccountTokensSlowInstanceCommand,
|
||||
];
|
||||
|
||||
+5
@@ -138,4 +138,9 @@ export class UpdateWorkspaceInput {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
useRecommendedModels?: boolean;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isInternalMessagesImportEnabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
aiAdditionalInstructions: PermissionFlagType.WORKSPACE,
|
||||
enabledAiModelIds: PermissionFlagType.AI_SETTINGS,
|
||||
useRecommendedModels: PermissionFlagType.AI_SETTINGS,
|
||||
isInternalMessagesImportEnabled: PermissionFlagType.WORKSPACE,
|
||||
};
|
||||
|
||||
constructor(
|
||||
|
||||
+1
@@ -33,6 +33,7 @@ export const fromWorkspaceEntityToFlat = (
|
||||
aiAdditionalInstructions: entity.aiAdditionalInstructions,
|
||||
enabledAiModelIds: entity.enabledAiModelIds,
|
||||
useRecommendedModels: entity.useRecommendedModels,
|
||||
isInternalMessagesImportEnabled: entity.isInternalMessagesImportEnabled,
|
||||
workspaceCustomApplicationId: entity.workspaceCustomApplicationId,
|
||||
routerModel: entity.routerModel,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
|
||||
@@ -278,6 +278,10 @@ export class WorkspaceEntity {
|
||||
@Column({ default: false })
|
||||
isCustomDomainEnabled: boolean;
|
||||
|
||||
@Field()
|
||||
@Column({ default: false })
|
||||
isInternalMessagesImportEnabled: boolean;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
|
||||
+6
-1
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
@@ -14,7 +15,11 @@ import { CreatePersonService } from 'src/modules/contact-creation-manager/servic
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceDataSourceModule,
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity, UserWorkspaceEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
FeatureFlagEntity,
|
||||
UserWorkspaceEntity,
|
||||
WorkspaceEntity,
|
||||
]),
|
||||
TypeOrmModule.forFeature([ObjectMetadataEntity, FieldMetadataEntity]),
|
||||
SecureHttpClientModule,
|
||||
],
|
||||
|
||||
+7
@@ -5,6 +5,7 @@ import { FieldActorSource } from 'twenty-shared/types';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { CreateCompanyAndPersonService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service';
|
||||
@@ -56,6 +57,12 @@ describe('CreateCompanyAndPersonService', () => {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
+9
@@ -14,6 +14,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
@@ -40,6 +41,8 @@ export class CreateCompanyAndPersonService {
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
async createCompaniesAndPeople(
|
||||
@@ -75,11 +78,17 @@ export class CreateCompanyAndPersonService {
|
||||
|
||||
const workspaceMembers = await workspaceMemberRepository.find();
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
select: ['id', 'isInternalMessagesImportEnabled'],
|
||||
});
|
||||
|
||||
const peopleToCreateFromOtherCompanies =
|
||||
filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contactsToCreate,
|
||||
connectedAccount,
|
||||
workspaceMembers,
|
||||
workspace?.isInternalMessagesImportEnabled ?? false,
|
||||
);
|
||||
|
||||
const { uniqueContacts, uniqueHandles } = getUniqueContactsAndHandles(
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { type Contact } from 'src/modules/contact-creation-manager/types/contact.type';
|
||||
import { filterOutContactsThatBelongToSelfOrWorkspaceMembers } from 'src/modules/contact-creation-manager/utils/filter-out-contacts-that-belong-to-self-or-workspace-members.util';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
const account = (
|
||||
handle: string,
|
||||
handleAliases: string[] = [],
|
||||
): ConnectedAccountEntity =>
|
||||
({ handle, handleAliases }) as ConnectedAccountEntity;
|
||||
|
||||
const contact = (handle: string): Contact => ({ handle, displayName: handle });
|
||||
|
||||
describe('filterOutContactsThatBelongToSelfOrWorkspaceMembers', () => {
|
||||
it('drops same-domain contacts by default for work domains', () => {
|
||||
const contacts = [contact('alice@acme.com'), contact('bob@other.com')];
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contacts,
|
||||
account('me@acme.com'),
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual([contact('bob@other.com')]);
|
||||
});
|
||||
|
||||
it('keeps same-domain contacts when isInternalMessagesImportEnabled is true', () => {
|
||||
const contacts = [contact('alice@acme.com'), contact('bob@other.com')];
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contacts,
|
||||
account('me@acme.com'),
|
||||
[],
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual(contacts);
|
||||
});
|
||||
|
||||
it('still drops workspace members and self even when flag is true', () => {
|
||||
const contacts = [
|
||||
contact('alice@acme.com'),
|
||||
contact('me@acme.com'),
|
||||
contact('member@acme.com'),
|
||||
];
|
||||
const workspaceMembers = [
|
||||
{ userEmail: 'member@acme.com' } as WorkspaceMemberWorkspaceEntity,
|
||||
];
|
||||
const result = filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contacts,
|
||||
account('me@acme.com'),
|
||||
workspaceMembers,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual([contact('alice@acme.com')]);
|
||||
});
|
||||
});
|
||||
+3
-1
@@ -10,6 +10,7 @@ export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
contacts: Contact[],
|
||||
connectedAccount: ConnectedAccountEntity,
|
||||
workspaceMembers: WorkspaceMemberWorkspaceEntity[],
|
||||
isInternalMessagesImportEnabled: boolean = false,
|
||||
): Contact[] {
|
||||
if (!isDefined(connectedAccount.handle)) {
|
||||
throw new Error('Connected account handle is missing');
|
||||
@@ -41,7 +42,8 @@ export function filterOutContactsThatBelongToSelfOrWorkspaceMembers(
|
||||
return contacts.filter(
|
||||
(contact) =>
|
||||
(isDifferentDomain(contact, selfDomainName) ||
|
||||
!isWorkDomain(selfDomainName)) &&
|
||||
!isWorkDomain(selfDomainName) ||
|
||||
isInternalMessagesImportEnabled) &&
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
!workspaceMembersMap[contact.handle.toLowerCase()] &&
|
||||
!allHandles.includes(contact.handle.toLowerCase()),
|
||||
|
||||
+9
@@ -26,6 +26,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
describe('MessagingMessagesImportService', () => {
|
||||
let service: MessagingMessagesImportService;
|
||||
@@ -183,6 +184,14 @@ describe('MessagingMessagesImportService', () => {
|
||||
findOne: jest.fn().mockResolvedValue({ userId: 'user-id' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(WorkspaceEntity),
|
||||
useValue: {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ isInternalMessagesImportEnabled: false }),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {
|
||||
|
||||
+9
@@ -9,6 +9,7 @@ import { CacheStorageService } from 'src/engine/core-modules/cache-storage/servi
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
@@ -55,6 +56,8 @@ export class MessagingMessagesImportService {
|
||||
private readonly messagingAccountAuthenticationService: MessagingAccountAuthenticationService,
|
||||
@InjectRepository(UserWorkspaceEntity)
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspaceEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
@@ -197,6 +200,11 @@ export class MessagingMessagesImportService {
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
select: ['id', 'isInternalMessagesImportEnabled'],
|
||||
});
|
||||
|
||||
const messagesToSave = filterEmails(
|
||||
messageChannel.handle,
|
||||
[...connectedAccountWithFreshTokens.handleAliases],
|
||||
@@ -205,6 +213,7 @@ export class MessagingMessagesImportService {
|
||||
.map((blocklistItem) => blocklistItem.handle)
|
||||
.filter(isDefined),
|
||||
messageChannel.excludeGroupEmails,
|
||||
workspace?.isInternalMessagesImportEnabled ?? false,
|
||||
);
|
||||
|
||||
if (messagesToSave.length > 0) {
|
||||
|
||||
+21
@@ -37,6 +37,27 @@ describe('filterEmails', () => {
|
||||
expect(filteredMessages).toEqual([]);
|
||||
});
|
||||
|
||||
it('Should keep same-domain emails when isInternalMessagesImportEnabled is true', () => {
|
||||
// Workspace opted into syncing internal emails (e.g. university or
|
||||
// shared-domain institution). Same-domain participants must not be
|
||||
// dropped — the toggle bypasses filterOutInternals.
|
||||
const primaryHandle = 'guillim@acme.com';
|
||||
const messages = messagingGetMessagesServiceGetMessages.filter(
|
||||
(message) => message.externalId === 'AA-work-emails-internal',
|
||||
);
|
||||
|
||||
const filteredMessages = filterEmails(
|
||||
primaryHandle,
|
||||
[],
|
||||
messages,
|
||||
[],
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(filteredMessages).toEqual(messages);
|
||||
});
|
||||
|
||||
it('Should filter messages with participant from the blocklist', () => {
|
||||
const primaryHandle = 'guillim@acme.com';
|
||||
const messages = messagingGetMessagesServiceGetMessages.filter(
|
||||
|
||||
+5
-1
@@ -15,6 +15,7 @@ export const filterEmails = (
|
||||
messages: MessageWithParticipants[],
|
||||
blocklist: string[],
|
||||
excludeGroupEmails: boolean = true,
|
||||
isInternalMessagesImportEnabled: boolean = false,
|
||||
) => {
|
||||
const messagesWithoutIcsAttachments = filterOutIcsAttachments(messages);
|
||||
|
||||
@@ -24,7 +25,10 @@ export const filterEmails = (
|
||||
blocklist,
|
||||
);
|
||||
|
||||
const messagesWithoutInternals = isWorkEmail(primaryHandle)
|
||||
const shouldFilterOutInternals =
|
||||
isWorkEmail(primaryHandle) && !isInternalMessagesImportEnabled;
|
||||
|
||||
const messagesWithoutInternals = shouldFilterOutInternals
|
||||
? filterOutInternals(primaryHandle, messagesWithoutBlocklisted)
|
||||
: messagesWithoutBlocklisted;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user