refactor(emailing): rename email* objects to message* prefix

Channel-agnostic naming ahead of SMS/text support: emailList->messageTopic,
emailListSubscription->messageSubscription, emailCampaign->messageBroadcast.
Regenerate client-sdk and front metadata graphql types.
This commit is contained in:
neo773
2026-06-02 21:21:08 +05:30
parent 3980094e02
commit 74e12df09c
16 changed files with 826 additions and 573 deletions
@@ -398,6 +398,7 @@ enum EngineComponentKey {
FRONT_COMPONENT_RENDERER
REPLY_TO_EMAIL_THREAD
COMPOSE_EMAIL
COMPOSE_CAMPAIGN
GO_TO_PEOPLE
GO_TO_COMPANIES
GO_TO_DASHBOARDS
@@ -1463,7 +1464,7 @@ enum EmailingDomainStatus {
}
type SendMessageBroadcastOutputDTO {
campaignId: String!
broadcastId: String!
sentCount: Int!
failedCount: Int!
}
@@ -3782,6 +3783,7 @@ input SendEmailViaDomainInput {
input SendMessageBroadcastInput {
messageTopicId: String!
recipientViewId: String
subject: String!
body: String!
fromAddress: String!
@@ -304,7 +304,7 @@ export interface CommandMenuItem {
__typename: 'CommandMenuItem'
}
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
export type EngineComponentKey = 'NAVIGATE_TO_NEXT_RECORD' | 'NAVIGATE_TO_PREVIOUS_RECORD' | 'CREATE_NEW_RECORD' | 'DELETE_RECORDS' | 'RESTORE_RECORDS' | 'DESTROY_RECORDS' | 'ADD_TO_FAVORITES' | 'REMOVE_FROM_FAVORITES' | 'EXPORT_NOTE_TO_PDF' | 'EXPORT_RECORDS' | 'UPDATE_MULTIPLE_RECORDS' | 'MERGE_MULTIPLE_RECORDS' | 'IMPORT_RECORDS' | 'EXPORT_VIEW' | 'SEE_DELETED_RECORDS' | 'CREATE_NEW_VIEW' | 'HIDE_DELETED_RECORDS' | 'EDIT_RECORD_PAGE_LAYOUT' | 'EDIT_DASHBOARD_LAYOUT' | 'SAVE_DASHBOARD_LAYOUT' | 'CANCEL_DASHBOARD_LAYOUT' | 'DUPLICATE_DASHBOARD' | 'ACTIVATE_WORKFLOW' | 'DEACTIVATE_WORKFLOW' | 'DISCARD_DRAFT_WORKFLOW' | 'TEST_WORKFLOW' | 'SEE_ACTIVE_VERSION_WORKFLOW' | 'SEE_RUNS_WORKFLOW' | 'SEE_VERSIONS_WORKFLOW' | 'ADD_NODE_WORKFLOW' | 'TIDY_UP_WORKFLOW' | 'DUPLICATE_WORKFLOW' | 'SEE_VERSION_WORKFLOW_RUN' | 'SEE_WORKFLOW_WORKFLOW_RUN' | 'STOP_WORKFLOW_RUN' | 'SEE_RUNS_WORKFLOW_VERSION' | 'SEE_WORKFLOW_WORKFLOW_VERSION' | 'USE_AS_DRAFT_WORKFLOW_VERSION' | 'SEE_VERSIONS_WORKFLOW_VERSION' | 'SEARCH_RECORDS' | 'SEARCH_RECORDS_FALLBACK' | 'ASK_AI' | 'VIEW_PREVIOUS_AI_CHATS' | 'NAVIGATION' | 'TRIGGER_WORKFLOW_VERSION' | 'FRONT_COMPONENT_RENDERER' | 'REPLY_TO_EMAIL_THREAD' | 'COMPOSE_EMAIL' | 'COMPOSE_CAMPAIGN' | 'GO_TO_PEOPLE' | 'GO_TO_COMPANIES' | 'GO_TO_DASHBOARDS' | 'GO_TO_OPPORTUNITIES' | 'GO_TO_SETTINGS' | 'GO_TO_TASKS' | 'GO_TO_NOTES' | 'GO_TO_WORKFLOWS' | 'GO_TO_RUNS' | 'DELETE_SINGLE_RECORD' | 'DELETE_MULTIPLE_RECORDS' | 'RESTORE_SINGLE_RECORD' | 'RESTORE_MULTIPLE_RECORDS' | 'DESTROY_SINGLE_RECORD' | 'DESTROY_MULTIPLE_RECORDS' | 'EXPORT_FROM_RECORD_INDEX' | 'EXPORT_FROM_RECORD_SHOW' | 'EXPORT_MULTIPLE_RECORDS'
export type CommandMenuItemAvailabilityType = 'GLOBAL' | 'GLOBAL_OBJECT_CONTEXT' | 'RECORD_SELECTION' | 'FALLBACK'
@@ -1119,7 +1119,7 @@ export type EmailingDomainDriver = 'AWS_SES'
export type EmailingDomainStatus = 'PENDING' | 'VERIFIED' | 'FAILED' | 'TEMPORARY_FAILURE'
export interface SendMessageBroadcastOutputDTO {
campaignId: Scalars['String']
broadcastId: Scalars['String']
sentCount: Scalars['Int']
failedCount: Scalars['Int']
__typename: 'SendMessageBroadcastOutputDTO'
@@ -4060,7 +4060,7 @@ export interface EmailingDomainGenqlSelection{
}
export interface SendMessageBroadcastOutputDTOGenqlSelection{
campaignId?: boolean | number
broadcastId?: boolean | number
sentCount?: boolean | number
failedCount?: boolean | number
__typename?: boolean | number
@@ -6107,7 +6107,7 @@ export interface GridPositionInput {row: Scalars['Float'],column: Scalars['Float
export interface SendEmailViaDomainInput {emailingDomainId: Scalars['String'],to: Scalars['String'][],cc?: (Scalars['String'][] | null),bcc?: (Scalars['String'][] | null),subject: Scalars['String'],text: Scalars['String'],html?: (Scalars['String'] | null),from: Scalars['String'],replyTo?: (Scalars['String'][] | null)}
export interface SendMessageBroadcastInput {messageTopicId: Scalars['String'],subject: Scalars['String'],body: Scalars['String'],fromAddress: Scalars['String']}
export interface SendMessageBroadcastInput {messageTopicId: Scalars['String'],recipientViewId?: (Scalars['String'] | null),subject: Scalars['String'],body: Scalars['String'],fromAddress: Scalars['String']}
export interface CreatePageLayoutWidgetInput {pageLayoutTabId: Scalars['UUID'],title: Scalars['String'],type: WidgetType,objectMetadataId?: (Scalars['UUID'] | null),gridPosition: GridPositionInput,position?: (Scalars['JSON'] | null),configuration: Scalars['JSON']}
@@ -8422,6 +8422,7 @@ export const enumEngineComponentKey = {
FRONT_COMPONENT_RENDERER: 'FRONT_COMPONENT_RENDERER' as const,
REPLY_TO_EMAIL_THREAD: 'REPLY_TO_EMAIL_THREAD' as const,
COMPOSE_EMAIL: 'COMPOSE_EMAIL' as const,
COMPOSE_CAMPAIGN: 'COMPOSE_CAMPAIGN' as const,
GO_TO_PEOPLE: 'GO_TO_PEOPLE' as const,
GO_TO_COMPANIES: 'GO_TO_COMPANIES' as const,
GO_TO_DASHBOARDS: 'GO_TO_DASHBOARDS' as const,
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@ import { Field, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class SendMessageBroadcastOutputDTO {
@Field(() => String)
campaignId: string;
broadcastId: string;
@Field(() => Int)
sentCount: number;
@@ -1,6 +1,13 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsEmail, IsString, IsUUID, Length, MinLength } from 'class-validator';
import {
IsEmail,
IsOptional,
IsString,
IsUUID,
Length,
MinLength,
} from 'class-validator';
@InputType()
export class SendMessageBroadcastInput {
@@ -8,6 +15,13 @@ export class SendMessageBroadcastInput {
@IsUUID('4')
messageTopicId: string;
// Optional Person view (segment) whose filters resolve the recipients.
// When omitted, recipients are the people subscribed to the topic.
@Field(() => String, { nullable: true })
@IsOptional()
@IsUUID('4')
recipientViewId?: string;
@Field(() => String)
@IsString()
@Length(1, 998)
@@ -103,9 +103,10 @@ export class EmailingDomainResolver {
@Args('input') input: SendMessageBroadcastInput,
@AuthWorkspace() currentWorkspace: WorkspaceEntity,
): Promise<SendMessageBroadcastOutputDTO> {
return this.messageBroadcastService.sendToList({
return this.messageBroadcastService.send({
workspaceId: currentWorkspace.id,
messageTopicId: input.messageTopicId,
recipientViewId: input.recipientViewId,
subject: input.subject,
html: input.body,
fromAddress: input.fromAddress,
@@ -1,4 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { isNonEmptyString } from '@sniptt/guards';
import { In } from 'typeorm';
@@ -7,6 +8,8 @@ import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/dr
import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
import { EmailingDomainSenderService } from 'src/engine/core-modules/emailing-domain/services/emailing-domain-sender.service';
import { EmailGroupMessageCategory } from 'src/engine/core-modules/emailing-domain/types/email-group-message-category.type';
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
import { ViewQueryParamsService } from 'src/engine/metadata-modules/view/services/view-query-params.service';
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -16,21 +19,25 @@ import { MessageSubscriptionWorkspaceEntity } from 'src/modules/emailing/standar
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { createHtmlToTextConverter } from 'src/modules/messaging/message-import-manager/utils/create-html-to-text-converter.util';
type SendCampaignArgs = {
type SendBroadcastArgs = {
workspaceId: string;
messageTopicId: string;
subject: string;
html: string;
fromAddress: string;
// When set, recipients come from this Person view's filters (a segment).
// Otherwise recipients are the people subscribed to the topic.
recipientViewId?: string;
};
type SendCampaignResult = {
campaignId: string;
type SendBroadcastResult = {
broadcastId: string;
sentCount: number;
failedCount: number;
};
const SUBSCRIBED_STATUS = 'SUBSCRIBED';
const PERSON_OBJECT_NAME = 'person';
@Injectable()
export class MessageBroadcastService {
@@ -42,15 +49,20 @@ export class MessageBroadcastService {
private readonly emailingDomainRepository: WorkspaceScopedRepository<EmailingDomainEntity>,
private readonly emailingDomainSenderService: EmailingDomainSenderService,
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
// Resolved lazily to avoid a module-load cycle: the record-crud/view
// graph (CoreCommonApiModule) cannot be eagerly imported from this
// early-loaded core module without a require-time TDZ.
private readonly moduleRef: ModuleRef,
) {}
async sendToList({
async send({
workspaceId,
messageTopicId,
subject,
html,
fromAddress,
}: SendCampaignArgs): Promise<SendCampaignResult> {
recipientViewId,
}: SendBroadcastArgs): Promise<SendBroadcastResult> {
const fromDomain = fromAddress.split('@')[1]?.toLowerCase();
const emailingDomain = await this.emailingDomainRepository.findOne(
@@ -68,28 +80,30 @@ export class MessageBroadcastService {
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
const recipientEmails = await this.resolveSubscribedEmails(
workspaceId,
messageTopicId,
);
const recipientEmails = isNonEmptyString(recipientViewId)
? await this.resolveRecipientsFromView(workspaceId, recipientViewId)
: await this.resolveSubscribedEmails(workspaceId, messageTopicId);
const campaignRepository =
const broadcastRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
MessageBroadcastWorkspaceEntity,
{ shouldBypassPermissionChecks: true },
);
const { identifiers } = await campaignRepository.insert({
const { identifiers } = await broadcastRepository.insert({
name: subject,
subject,
bodyTemplate: html,
fromAddress,
status: 'SENDING',
recipientSource: 'LIST',
listId: messageTopicId,
recipientSource: isNonEmptyString(recipientViewId)
? 'FILTER'
: 'LIST',
recipientViewId: recipientViewId ?? null,
topicId: messageTopicId,
});
const campaignId = identifiers[0].id;
const broadcastId = identifiers[0].id;
let sentCount = 0;
let failedCount = 0;
@@ -113,26 +127,69 @@ export class MessageBroadcastService {
} catch (error) {
failedCount += 1;
this.logger.warn(
`Campaign ${campaignId} skipped ${recipientEmail}: ${
`Broadcast ${broadcastId} skipped ${recipientEmail}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
await campaignRepository.update(campaignId, {
await broadcastRepository.update(broadcastId, {
status: 'SENT',
sentAt: new Date(),
sentCount,
failedCount,
});
return { campaignId, sentCount, failedCount };
return { broadcastId, sentCount, failedCount };
},
buildSystemAuthContext(workspaceId),
);
}
// Resolves the recipients of a broadcast from a saved Person view (segment):
// the view's filters are run server-side to produce the list of people.
private async resolveRecipientsFromView(
workspaceId: string,
recipientViewId: string,
): Promise<string[]> {
const viewQueryParamsService = this.moduleRef.get(ViewQueryParamsService, {
strict: false,
});
const findRecordsService = this.moduleRef.get(FindRecordsService, {
strict: false,
});
const viewParams = await viewQueryParamsService.resolveViewToQueryParams(
recipientViewId,
workspaceId,
);
if (viewParams.objectNameSingular !== PERSON_OBJECT_NAME) {
throw new Error(
`Recipient view must target ${PERSON_OBJECT_NAME} records, got ${viewParams.objectNameSingular}`,
);
}
const output = await findRecordsService.execute({
objectName: PERSON_OBJECT_NAME,
filter: viewParams.filter,
authContext: buildSystemAuthContext(workspaceId),
});
if (!output.success || !output.result) {
return [];
}
const records = output.result.records as Array<{
emails?: { primaryEmail?: string | null };
}>;
return records
.map((record) => record.emails?.primaryEmail)
.filter((email): email is string => isNonEmptyString(email));
}
private async resolveSubscribedEmails(
workspaceId: string,
messageTopicId: string,
@@ -145,7 +202,7 @@ export class MessageBroadcastService {
);
const subscriptions = await subscriptionRepository.find({
where: { listId: messageTopicId, status: SUBSCRIBED_STATUS },
where: { topicId: messageTopicId, status: SUBSCRIBED_STATUS },
});
const personIds = subscriptions.map(
@@ -286,7 +286,7 @@ export const buildMessageBroadcastStandardFlatFieldMetadatas = ({
{
id: '89f0301d-b168-4da3-b435-4bd5e969b604',
value: 'LIST',
label: i18nLabel(msg`List`),
label: i18nLabel(msg`Topic`),
position: 0,
color: 'blue',
},
@@ -300,6 +300,19 @@ export const buildMessageBroadcastStandardFlatFieldMetadatas = ({
],
},
}),
recipientViewId: createStandardFieldFlatMetadata({
...base,
context: {
fieldName: 'recipientViewId',
type: FieldMetadataType.UUID,
label: i18nLabel(msg`Recipient view`),
description: i18nLabel(
msg`The Person view whose filters resolve the recipients`,
),
icon: 'IconFilter',
isNullable: true,
},
}),
scheduledAt: createStandardFieldFlatMetadata({
...base,
context: {
@@ -360,22 +373,22 @@ export const buildMessageBroadcastStandardFlatFieldMetadatas = ({
defaultValue: 0,
},
}),
list: createStandardRelationFieldFlatMetadata({
topic: createStandardRelationFieldFlatMetadata({
...base,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'list',
label: i18nLabel(msg`List`),
fieldName: 'topic',
label: i18nLabel(msg`Topic`),
description: i18nLabel(msg`The audience this campaign was sent to`),
icon: 'IconMailbox',
isNullable: true,
targetObjectName: 'messageTopic',
targetFieldName: 'campaigns',
targetFieldName: 'broadcasts',
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.SET_NULL,
joinColumnName: 'listId',
joinColumnName: 'topicId',
},
},
}),
@@ -174,6 +174,23 @@ export const buildMessageSubscriptionStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
topicName: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'topicName',
type: FieldMetadataType.TEXT,
label: i18nLabel(msg`Topic`),
description: i18nLabel(msg`The topic this subscription is for`),
icon: 'IconMailbox',
isNullable: true,
isUIReadOnly: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
status: createStandardFieldFlatMetadata({
objectName,
workspaceId,
@@ -181,7 +198,7 @@ export const buildMessageSubscriptionStandardFlatFieldMetadatas = ({
fieldName: 'status',
type: FieldMetadataType.SELECT,
label: i18nLabel(msg`Status`),
description: i18nLabel(msg`Subscription status for the list`),
description: i18nLabel(msg`Subscription status for the topic`),
icon: 'IconMailShare',
isNullable: false,
isUIReadOnly: true,
@@ -292,14 +309,14 @@ export const buildMessageSubscriptionStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
list: createStandardRelationFieldFlatMetadata({
topic: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'list',
label: i18nLabel(msg`List`),
fieldName: 'topic',
label: i18nLabel(msg`Topic`),
description: i18nLabel(msg`The list the person is subscribed to`),
icon: 'IconMailbox',
isNullable: false,
@@ -308,7 +325,7 @@ export const buildMessageSubscriptionStandardFlatFieldMetadatas = ({
settings: {
relationType: RelationType.MANY_TO_ONE,
onDelete: RelationOnDeleteAction.CASCADE,
joinColumnName: 'listId',
joinColumnName: 'topicId',
},
},
standardObjectMetadataRelatedEntityIds,
@@ -201,7 +201,7 @@ export const buildMessageTopicStandardFlatFieldMetadatas = ({
fieldName: 'name',
type: FieldMetadataType.TEXT,
label: i18nLabel(msg`Name`),
description: i18nLabel(msg`The email list name`),
description: i18nLabel(msg`The topic public display name`),
icon: 'IconMailbox',
isNullable: true,
},
@@ -210,6 +210,94 @@ export const buildMessageTopicStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
description: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'description',
type: FieldMetadataType.TEXT,
label: i18nLabel(msg`Description`),
description: i18nLabel(
msg`Public description shown on the preferences page`,
),
icon: 'IconFileDescription',
isNullable: true,
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
subscriptionDefault: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'subscriptionDefault',
type: FieldMetadataType.SELECT,
label: i18nLabel(msg`Defaults to`),
description: i18nLabel(
msg`Whether contacts are subscribed to this topic by default`,
),
icon: 'IconToggleRight',
isNullable: false,
defaultValue: "'OPT_IN'",
options: [
{
id: '9c23720f-c250-44e3-bf35-d6f5a1fd2d47',
value: 'OPT_IN',
label: i18nLabel(msg`Opt-in`),
position: 0,
color: 'green',
},
{
id: 'cae4baf5-b81f-4a6d-bcc3-8015ddaeed12',
value: 'OPT_OUT',
label: i18nLabel(msg`Opt-out`),
position: 1,
color: 'orange',
},
],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
visibility: createStandardFieldFlatMetadata({
objectName,
workspaceId,
context: {
fieldName: 'visibility',
type: FieldMetadataType.SELECT,
label: i18nLabel(msg`Visibility`),
description: i18nLabel(
msg`Whether the topic is shown on the public preferences page`,
),
icon: 'IconEye',
isNullable: false,
defaultValue: "'PRIVATE'",
options: [
{
id: 'c919e1ad-75a9-4e3b-a8ad-05fde89e6146',
value: 'PUBLIC',
label: i18nLabel(msg`Public`),
position: 0,
color: 'blue',
},
{
id: 'f8568bfb-93da-40c2-b70e-e1fbc9d503f1',
value: 'PRIVATE',
label: i18nLabel(msg`Private`),
position: 1,
color: 'gray',
},
],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
subscriptions: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
@@ -222,7 +310,7 @@ export const buildMessageTopicStandardFlatFieldMetadatas = ({
icon: 'IconMailShare',
isNullable: true,
targetObjectName: 'messageSubscription',
targetFieldName: 'list',
targetFieldName: 'topic',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
@@ -232,20 +320,20 @@ export const buildMessageTopicStandardFlatFieldMetadatas = ({
twentyStandardApplicationId,
now,
}),
campaigns: createStandardRelationFieldFlatMetadata({
broadcasts: createStandardRelationFieldFlatMetadata({
objectName,
workspaceId,
context: {
type: FieldMetadataType.RELATION,
morphId: null,
fieldName: 'campaigns',
fieldName: 'broadcasts',
label: i18nLabel(msg`Campaigns`),
description: i18nLabel(msg`Campaigns sent to this list`),
icon: 'IconSend',
isUIReadOnly: true,
isNullable: true,
targetObjectName: 'messageBroadcast',
targetFieldName: 'list',
targetFieldName: 'topic',
settings: {
relationType: RelationType.ONE_TO_MANY,
},
@@ -17,12 +17,12 @@ export const buildMessageBroadcastStandardFlatIndexMetadatas = ({
AllStandardObjectIndexName<'messageBroadcast'>,
FlatIndexMetadata
> => ({
listIdIndex: createStandardIndexFlatMetadata({
topicIdIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'listIdIndex',
relatedFieldNames: ['list'],
indexName: 'topicIdIndex',
relatedFieldNames: ['topic'],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
@@ -16,24 +16,24 @@ export const buildMessageSubscriptionStandardFlatIndexMetadatas = ({
AllStandardObjectIndexName<'messageSubscription'>,
FlatIndexMetadata
> => ({
listIdIndex: createStandardIndexFlatMetadata({
topicIdIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'listIdIndex',
relatedFieldNames: ['list'],
indexName: 'topicIdIndex',
relatedFieldNames: ['topic'],
},
standardObjectMetadataRelatedEntityIds,
dependencyFlatEntityMaps,
twentyStandardApplicationId,
now,
}),
personListUniqueIndex: createStandardIndexFlatMetadata({
personTopicUniqueIndex: createStandardIndexFlatMetadata({
objectName,
workspaceId,
context: {
indexName: 'personListUniqueIndex',
relatedFieldNames: ['person', 'list'],
indexName: 'personTopicUniqueIndex',
relatedFieldNames: ['person', 'topic'],
isUnique: true,
},
standardObjectMetadataRelatedEntityIds,
@@ -21,12 +21,13 @@ export class MessageBroadcastWorkspaceEntity extends BaseWorkspaceEntity {
replyTo: string | null;
status: string;
recipientSource: string;
recipientViewId: string | null;
scheduledAt: Date | null;
sentAt: Date | null;
sentCount: number;
bouncedCount: number;
failedCount: number;
list: EntityRelation<MessageTopicWorkspaceEntity> | null;
listId: string | null;
topic: EntityRelation<MessageTopicWorkspaceEntity> | null;
topicId: string | null;
searchVector: string;
}
@@ -4,12 +4,15 @@ import { type MessageTopicWorkspaceEntity } from 'src/modules/emailing/standard-
import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
export class MessageSubscriptionWorkspaceEntity extends BaseWorkspaceEntity {
// Denormalized copy of the topic name so the subscription is identifiable
// (its labelIdentifier) — a relation can't be a labelIdentifier in Twenty.
topicName: string | null;
status: string;
subscribedAt: Date | null;
unsubscribedAt: Date | null;
source: string;
list: EntityRelation<MessageTopicWorkspaceEntity>;
listId: string;
topic: EntityRelation<MessageTopicWorkspaceEntity>;
topicId: string;
person: EntityRelation<PersonWorkspaceEntity>;
personId: string;
searchVector: string;
@@ -14,7 +14,10 @@ export const SEARCH_FIELDS_FOR_MESSAGE_TOPIC: FieldTypeAndNameMetadata[] = [
export class MessageTopicWorkspaceEntity extends BaseWorkspaceEntity {
name: string | null;
description: string | null;
subscriptionDefault: string;
visibility: string;
subscriptions: EntityRelation<MessageSubscriptionWorkspaceEntity[]>;
campaigns: EntityRelation<MessageBroadcastWorkspaceEntity[]>;
broadcasts: EntityRelation<MessageBroadcastWorkspaceEntity[]>;
searchVector: string;
}