Feat/email settings v2 (#23180)

Settings pages changes

- Add `displayName`
- Unsubscribers Page

<img width="1496" height="844" alt="Screenshot 2026-07-22 at 8 52 15 PM"
src="https://github.com/user-attachments/assets/69bc1993-4547-4a64-83a6-b47fef1a4e40"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23180?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
neo773
2026-07-27 02:18:30 +05:30
committed by GitHub
parent 8326fd186f
commit 3fb29db28a
49 changed files with 1913 additions and 389 deletions
@@ -2499,6 +2499,7 @@ type MessageChannel {
id: UUID!
visibility: MessageChannelVisibility!
handle: String!
displayName: String
type: MessageChannelType!
isContactAutoCreationEnabled: Boolean!
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy!
@@ -2597,6 +2598,31 @@ type SendMessageCampaignOutputDTO {
skipped: CampaignSkippedRecipientsDTO!
}
type MessageSuppression {
id: UUID!
createdAt: DateTime!
emailAddress: String!
reason: MessageSuppressionReason!
source: MessageSuppressionSource!
unsubscribeTopicId: UUID
}
enum MessageSuppressionReason {
BOUNCE
COMPLAINT
UNSUBSCRIBE
}
enum MessageSuppressionSource {
WEBHOOK
SYSTEM
}
type MessageSuppressionList {
records: [MessageSuppression!]!
totalCount: Int!
}
type UnsubscribeTopic {
id: UUID!
createdAt: DateTime!
@@ -3157,8 +3183,8 @@ type Query {
getViewGroup(id: String!): ViewGroup
getRoles: [Role!]!
previewMessageCampaignAudience(input: PreviewMessageCampaignAudienceInput!): CampaignAudiencePreviewDTO!
messageSuppressions(input: FindMessageSuppressionsInput!): MessageSuppressionList!
unsubscribeTopics: [UnsubscribeTopic!]!
unsubscribePagePreviewUrl: String!
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
getEmailingDomains: [EmailingDomain!]!
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
@@ -3232,6 +3258,14 @@ input PreviewMessageCampaignAudienceInput {
unsubscribeTopicId: String
}
input FindMessageSuppressionsInput {
reason: MessageSuppressionReason
searchTerm: String
unsubscribeTopicId: UUID
limit: Int! = 30
offset: Int! = 0
}
input ListAppConnectionsInput {
providerName: String
userWorkspaceId: String
@@ -3448,6 +3482,7 @@ type Mutation {
deleteUnsubscribeTopic(id: String!): Boolean!
updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel!
createEmailGroupChannel(input: CreateEmailGroupChannelInput!): CreateEmailGroupChannelOutput!
updateEmailGroupChannel(input: UpdateEmailGroupChannelInput!): MessageChannel!
deleteEmailGroupChannel(id: UUID!): MessageChannel!
createEmailingDomain(input: CreateEmailingDomainInput!): EmailingDomain!
deleteEmailingDomain(id: String!): Boolean!
@@ -4532,6 +4567,12 @@ input UpdateMessageChannelInputUpdates {
input CreateEmailGroupChannelInput {
handle: String!
displayName: String
}
input UpdateEmailGroupChannelInput {
id: UUID!
displayName: String
}
input CreateEmailingDomainInput {
@@ -2201,6 +2201,7 @@ export interface MessageChannel {
id: Scalars['UUID']
visibility: MessageChannelVisibility
handle: Scalars['String']
displayName?: Scalars['String']
type: MessageChannelType
isContactAutoCreationEnabled: Scalars['Boolean']
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy
@@ -2271,6 +2272,26 @@ export interface SendMessageCampaignOutputDTO {
__typename: 'SendMessageCampaignOutputDTO'
}
export interface MessageSuppression {
id: Scalars['UUID']
createdAt: Scalars['DateTime']
emailAddress: Scalars['String']
reason: MessageSuppressionReason
source: MessageSuppressionSource
unsubscribeTopicId?: Scalars['UUID']
__typename: 'MessageSuppression'
}
export type MessageSuppressionReason = 'BOUNCE' | 'COMPLAINT' | 'UNSUBSCRIBE'
export type MessageSuppressionSource = 'WEBHOOK' | 'SYSTEM'
export interface MessageSuppressionList {
records: MessageSuppression[]
totalCount: Scalars['Int']
__typename: 'MessageSuppressionList'
}
export interface UnsubscribeTopic {
id: Scalars['UUID']
createdAt: Scalars['DateTime']
@@ -2793,8 +2814,8 @@ export interface Query {
getViewGroup?: ViewGroup
getRoles: Role[]
previewMessageCampaignAudience: CampaignAudiencePreviewDTO
messageSuppressions: MessageSuppressionList
unsubscribeTopics: UnsubscribeTopic[]
unsubscribePagePreviewUrl: Scalars['String']
myMessageChannels: MessageChannel[]
getEmailingDomains: EmailingDomain[]
myConnectedAccounts: ConnectedAccountPublicDTO[]
@@ -2992,6 +3013,7 @@ export interface Mutation {
deleteUnsubscribeTopic: Scalars['Boolean']
updateMessageChannel: MessageChannel
createEmailGroupChannel: CreateEmailGroupChannelOutput
updateEmailGroupChannel: MessageChannel
deleteEmailGroupChannel: MessageChannel
createEmailingDomain: EmailingDomain
deleteEmailingDomain: Scalars['Boolean']
@@ -5396,6 +5418,7 @@ export interface MessageChannelGenqlSelection{
id?: boolean | number
visibility?: boolean | number
handle?: boolean | number
displayName?: boolean | number
type?: boolean | number
isContactAutoCreationEnabled?: boolean | number
contactAutoCreationPolicy?: boolean | number
@@ -5458,6 +5481,24 @@ export interface SendMessageCampaignOutputDTOGenqlSelection{
__scalar?: boolean | number
}
export interface MessageSuppressionGenqlSelection{
id?: boolean | number
createdAt?: boolean | number
emailAddress?: boolean | number
reason?: boolean | number
source?: boolean | number
unsubscribeTopicId?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MessageSuppressionListGenqlSelection{
records?: MessageSuppressionGenqlSelection
totalCount?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface UnsubscribeTopicGenqlSelection{
id?: boolean | number
createdAt?: boolean | number
@@ -6020,8 +6061,8 @@ export interface QueryGenqlSelection{
getViewGroup?: (ViewGroupGenqlSelection & { __args: {id: Scalars['String']} })
getRoles?: RoleGenqlSelection
previewMessageCampaignAudience?: (CampaignAudiencePreviewDTOGenqlSelection & { __args: {input: PreviewMessageCampaignAudienceInput} })
messageSuppressions?: (MessageSuppressionListGenqlSelection & { __args: {input: FindMessageSuppressionsInput} })
unsubscribeTopics?: UnsubscribeTopicGenqlSelection
unsubscribePagePreviewUrl?: boolean | number
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
getEmailingDomains?: EmailingDomainGenqlSelection
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
@@ -6079,6 +6120,8 @@ id: Scalars['ID']}
export interface PreviewMessageCampaignAudienceInput {listId: Scalars['String'],unsubscribeTopicId?: (Scalars['String'] | null)}
export interface FindMessageSuppressionsInput {reason?: (MessageSuppressionReason | null),searchTerm?: (Scalars['String'] | null),unsubscribeTopicId?: (Scalars['UUID'] | null),limit: Scalars['Int'],offset: Scalars['Int']}
export interface ListAppConnectionsInput {providerName?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),visibility?: (Scalars['String'] | null)}
export interface EventLogQueryInput {table: EventLogTable,filters?: (EventLogFiltersInput | null),first?: (Scalars['Int'] | null),after?: (Scalars['String'] | null)}
@@ -6246,6 +6289,7 @@ export interface MutationGenqlSelection{
deleteUnsubscribeTopic?: { __args: {id: Scalars['String']} }
updateMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: UpdateMessageChannelInput} })
createEmailGroupChannel?: (CreateEmailGroupChannelOutputGenqlSelection & { __args: {input: CreateEmailGroupChannelInput} })
updateEmailGroupChannel?: (MessageChannelGenqlSelection & { __args: {input: UpdateEmailGroupChannelInput} })
deleteEmailGroupChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
createEmailingDomain?: (EmailingDomainGenqlSelection & { __args: {input: CreateEmailingDomainInput} })
deleteEmailingDomain?: { __args: {id: Scalars['String']} }
@@ -6655,7 +6699,9 @@ export interface UpdateMessageChannelInput {id: Scalars['UUID'],update: UpdateMe
export interface UpdateMessageChannelInputUpdates {visibility?: (MessageChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (MessageChannelContactAutoCreationPolicy | null),messageFolderImportPolicy?: (MessageFolderImportPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null),excludeNonProfessionalEmails?: (Scalars['Boolean'] | null),excludeGroupEmails?: (Scalars['Boolean'] | null)}
export interface CreateEmailGroupChannelInput {handle: Scalars['String']}
export interface CreateEmailGroupChannelInput {handle: Scalars['String'],displayName?: (Scalars['String'] | null)}
export interface UpdateEmailGroupChannelInput {id: Scalars['UUID'],displayName?: (Scalars['String'] | null)}
export interface CreateEmailingDomainInput {domain: Scalars['String']}
@@ -8409,6 +8455,22 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const MessageSuppression_possibleTypes: string[] = ['MessageSuppression']
export const isMessageSuppression = (obj?: { __typename?: any } | null): obj is MessageSuppression => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMessageSuppression"')
return MessageSuppression_possibleTypes.includes(obj.__typename)
}
const MessageSuppressionList_possibleTypes: string[] = ['MessageSuppressionList']
export const isMessageSuppressionList = (obj?: { __typename?: any } | null): obj is MessageSuppressionList => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMessageSuppressionList"')
return MessageSuppressionList_possibleTypes.includes(obj.__typename)
}
const UnsubscribeTopic_possibleTypes: string[] = ['UnsubscribeTopic']
export const isUnsubscribeTopic = (obj?: { __typename?: any } | null): obj is UnsubscribeTopic => {
if (!obj?.__typename) throw new Error('__typename is missing in "isUnsubscribeTopic"')
@@ -9386,6 +9448,17 @@ export const enumMessageChannelSyncStage = {
FAILED: 'FAILED' as const
}
export const enumMessageSuppressionReason = {
BOUNCE: 'BOUNCE' as const,
COMPLAINT: 'COMPLAINT' as const,
UNSUBSCRIBE: 'UNSUBSCRIBE' as const
}
export const enumMessageSuppressionSource = {
WEBHOOK: 'WEBHOOK' as const,
SYSTEM: 'SYSTEM' as const
}
export const enumUnsubscribeTopicVisibility = {
PUBLIC: 'PUBLIC' as const,
PRIVATE: 'PRIVATE' as const
File diff suppressed because it is too large Load Diff
@@ -1137,6 +1137,7 @@ export type CreateCommandMenuItemInput = {
};
export type CreateEmailGroupChannelInput = {
displayName?: InputMaybe<Scalars['String']['input']>;
handle: Scalars['String']['input'];
};
@@ -1971,6 +1972,14 @@ export type FindAvailableSsoidp = {
workspace: WorkspaceNameAndId;
};
export type FindMessageSuppressionsInput = {
limit?: Scalars['Int']['input'];
offset?: Scalars['Int']['input'];
reason?: InputMaybe<MessageSuppressionReason>;
searchTerm?: InputMaybe<Scalars['String']['input']>;
unsubscribeTopicId?: InputMaybe<Scalars['UUID']['input']>;
};
export type FrontComponent = {
__typename?: 'FrontComponent';
applicationId: Scalars['UUID']['output'];
@@ -2369,6 +2378,7 @@ export type MessageChannel = {
connectedAccountId: Scalars['UUID']['output'];
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
createdAt: Scalars['DateTime']['output'];
displayName?: Maybe<Scalars['String']['output']>;
excludeGroupEmails: Scalars['Boolean']['output'];
excludeNonProfessionalEmails: Scalars['Boolean']['output'];
handle: Scalars['String']['output'];
@@ -2456,6 +2466,33 @@ export enum MessageFolderPendingSyncAction {
NONE = 'NONE'
}
export type MessageSuppression = {
__typename?: 'MessageSuppression';
createdAt: Scalars['DateTime']['output'];
emailAddress: Scalars['String']['output'];
id: Scalars['UUID']['output'];
reason: MessageSuppressionReason;
source: MessageSuppressionSource;
unsubscribeTopicId?: Maybe<Scalars['UUID']['output']>;
};
export type MessageSuppressionList = {
__typename?: 'MessageSuppressionList';
records: Array<MessageSuppression>;
totalCount: Scalars['Int']['output'];
};
export enum MessageSuppressionReason {
BOUNCE = 'BOUNCE',
COMPLAINT = 'COMPLAINT',
UNSUBSCRIBE = 'UNSUBSCRIBE'
}
export enum MessageSuppressionSource {
SYSTEM = 'SYSTEM',
WEBHOOK = 'WEBHOOK'
}
export type MetadataEvent = {
__typename?: 'MetadataEvent';
metadataName: Scalars['String']['output'];
@@ -2685,6 +2722,7 @@ export type Mutation = {
updateApplicationRegistrationVariable: ApplicationRegistrationVariable;
updateCalendarChannel: CalendarChannel;
updateCommandMenuItem: CommandMenuItem;
updateEmailGroupChannel: MessageChannel;
updateFrontComponent: FrontComponent;
updateLabPublicFeatureFlag: FeatureFlag;
updateManyNavigationMenuItems: Array<NavigationMenuItem>;
@@ -3592,6 +3630,11 @@ export type MutationUpdateCommandMenuItemArgs = {
};
export type MutationUpdateEmailGroupChannelArgs = {
input: UpdateEmailGroupChannelInput;
};
export type MutationUpdateFrontComponentArgs = {
input: UpdateFrontComponentInput;
};
@@ -4457,6 +4500,7 @@ export type Query = {
isApplicationStopped: Scalars['Boolean']['output'];
lineChartData: LineChartData;
listPlans: Array<BillingPlan>;
messageSuppressions: MessageSuppressionList;
minimalMetadata: MinimalMetadata;
mostlyEmptyFieldMetadataIds: Array<Scalars['UUID']['output']>;
myCalendarChannels: Array<CalendarChannel>;
@@ -4474,7 +4518,6 @@ export type Query = {
publicMarketplaceApps: Array<MarketplaceApp>;
skill?: Maybe<Skill>;
skills: Array<Skill>;
unsubscribePagePreviewUrl: Scalars['String']['output'];
unsubscribeTopics: Array<UnsubscribeTopic>;
validatePasswordResetToken: ValidatePasswordResetToken;
webhook?: Maybe<Webhook>;
@@ -4829,6 +4872,11 @@ export type QueryLineChartDataArgs = {
};
export type QueryMessageSuppressionsArgs = {
input: FindMessageSuppressionsInput;
};
export type QueryMostlyEmptyFieldMetadataIdsArgs = {
objectMetadataId: Scalars['UUID']['input'];
};
@@ -5459,6 +5507,11 @@ export type UpdateCommandMenuItemInput = {
shortLabel?: InputMaybe<Scalars['String']['input']>;
};
export type UpdateEmailGroupChannelInput = {
displayName?: InputMaybe<Scalars['String']['input']>;
id: Scalars['UUID']['input'];
};
export type UpdateFieldInput = {
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
description?: InputMaybe<Scalars['String']['input']>;
@@ -7817,6 +7870,13 @@ export type UpdateCalendarChannelMutationVariables = Exact<{
export type UpdateCalendarChannelMutation = { __typename?: 'Mutation', updateCalendarChannel: { __typename?: 'CalendarChannel', id: string, visibility: CalendarChannelVisibility, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy } };
export type UpdateEmailGroupChannelMutationVariables = Exact<{
input: UpdateEmailGroupChannelInput;
}>;
export type UpdateEmailGroupChannelMutation = { __typename?: 'Mutation', updateEmailGroupChannel: { __typename?: 'MessageChannel', id: string, displayName?: string | null } };
export type UpdateMessageChannelMutationVariables = Exact<{
input: UpdateMessageChannelInput;
}>;
@@ -7862,7 +7922,7 @@ export type MyMessageChannelsQueryVariables = Exact<{
}>;
export type MyMessageChannelsQuery = { __typename?: 'Query', myMessageChannels: Array<{ __typename?: 'MessageChannel', id: string, handle: string, visibility: MessageChannelVisibility, type: MessageChannelType, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy, messageFolderImportPolicy: MessageFolderImportPolicy, excludeNonProfessionalEmails: boolean, excludeGroupEmails: boolean, isSyncEnabled: boolean, syncStatus: MessageChannelSyncStatus, syncStage: MessageChannelSyncStage, syncStageStartedAt?: string | null, connectedAccountId: string, createdAt: string, updatedAt: string, connectedAccount?: { __typename?: 'ConnectedAccountPublicDTO', id: string, handle: string } | null }> };
export type MyMessageChannelsQuery = { __typename?: 'Query', myMessageChannels: Array<{ __typename?: 'MessageChannel', id: string, handle: string, displayName?: string | null, visibility: MessageChannelVisibility, type: MessageChannelType, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy, messageFolderImportPolicy: MessageFolderImportPolicy, excludeNonProfessionalEmails: boolean, excludeGroupEmails: boolean, isSyncEnabled: boolean, syncStatus: MessageChannelSyncStatus, syncStage: MessageChannelSyncStage, syncStageStartedAt?: string | null, connectedAccountId: string, createdAt: string, updatedAt: string, connectedAccount?: { __typename?: 'ConnectedAccountPublicDTO', id: string, handle: string } | null }> };
export type MyMessageFoldersQueryVariables = Exact<{
messageChannelId?: InputMaybe<Scalars['UUID']['input']>;
@@ -8473,10 +8533,12 @@ export type UpdateUnsubscribeTopicMutationVariables = Exact<{
export type UpdateUnsubscribeTopicMutation = { __typename?: 'Mutation', updateUnsubscribeTopic: { __typename?: 'UnsubscribeTopic', id: string, name?: string | null, description?: string | null, visibility: UnsubscribeTopicVisibility } };
export type UnsubscribePagePreviewUrlQueryVariables = Exact<{ [key: string]: never; }>;
export type MessageSuppressionsQueryVariables = Exact<{
input: FindMessageSuppressionsInput;
}>;
export type UnsubscribePagePreviewUrlQuery = { __typename?: 'Query', unsubscribePagePreviewUrl: string };
export type MessageSuppressionsQuery = { __typename?: 'Query', messageSuppressions: { __typename?: 'MessageSuppressionList', totalCount: number, records: Array<{ __typename?: 'MessageSuppression', id: string, createdAt: string, emailAddress: string, reason: MessageSuppressionReason, unsubscribeTopicId?: string | null }> } };
export type GetUsageAnalyticsQueryVariables = Exact<{
input?: InputMaybe<UsageAnalyticsInput>;
@@ -9106,13 +9168,14 @@ export const DeleteEmailGroupChannelDocument = {"kind":"Document","definitions":
export const SaveImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SaveImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"handle"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectionParameters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EmailAccountConnectionParameters"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"saveImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"handle"},"value":{"kind":"Variable","name":{"kind":"Name","value":"handle"}}},{"kind":"Argument","name":{"kind":"Name","value":"connectionParameters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectionParameters"}}},{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}}]}}]}}]} as unknown as DocumentNode<SaveImapSmtpCaldavAccountMutation, SaveImapSmtpCaldavAccountMutationVariables>;
export const StartChannelSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StartChannelSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"startChannelSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode<StartChannelSyncMutation, StartChannelSyncMutationVariables>;
export const UpdateCalendarChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCalendarChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateCalendarChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCalendarChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}}]}}]}}]} as unknown as DocumentNode<UpdateCalendarChannelMutation, UpdateCalendarChannelMutationVariables>;
export const UpdateEmailGroupChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateEmailGroupChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateEmailGroupChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateEmailGroupChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}}]}}]}}]} as unknown as DocumentNode<UpdateEmailGroupChannelMutation, UpdateEmailGroupChannelMutationVariables>;
export const UpdateMessageChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}}]}}]}}]} as unknown as DocumentNode<UpdateMessageChannelMutation, UpdateMessageChannelMutationVariables>;
export const UpdateMessageFolderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFolderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode<UpdateMessageFolderMutation, UpdateMessageFolderMutationVariables>;
export const UpdateMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFoldersInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSyncAction"}}]}}]}}]} as unknown as DocumentNode<UpdateMessageFoldersMutation, UpdateMessageFoldersMutationVariables>;
export const GetConnectedImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectedImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getConnectedImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetConnectedImapSmtpCaldavAccountQuery, GetConnectedImapSmtpCaldavAccountQueryVariables>;
export const MyCalendarChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyCalendarChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myCalendarChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyCalendarChannelsQuery, MyCalendarChannelsQueryVariables>;
export const MyConnectedAccountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"authFailedAt"}},{"kind":"Field","name":{"kind":"Name","value":"archivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"}},{"kind":"Field","name":{"kind":"Name","value":"handleAliases"}},{"kind":"Field","name":{"kind":"Name","value":"lastSignedInAt"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionProviderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"lastCredentialsRefreshedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyConnectedAccountsQuery, MyConnectedAccountsQueryVariables>;
export const MyMessageChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyMessageChannelsQuery, MyMessageChannelsQueryVariables>;
export const MyMessageChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyMessageChannelsQuery, MyMessageChannelsQueryVariables>;
export const MyMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"messageChannelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"isSentFolder"}},{"kind":"Field","name":{"kind":"Name","value":"parentFolderId"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSyncAction"}},{"kind":"Field","name":{"kind":"Name","value":"messageChannelId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyMessageFoldersQuery, MyMessageFoldersQueryVariables>;
export const ClaimApplicationRegistrationOwnershipDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ClaimApplicationRegistrationOwnership"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"claimApplicationRegistrationOwnership"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode<ClaimApplicationRegistrationOwnershipMutation, ClaimApplicationRegistrationOwnershipMutationVariables>;
export const DeleteApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<DeleteApplicationRegistrationMutation, DeleteApplicationRegistrationMutationVariables>;
@@ -9197,7 +9260,7 @@ export const VerifyTwoFactorAuthenticationMethodForAuthenticatedUserDocument = {
export const CreateUnsubscribeTopicDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateUnsubscribeTopic"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateUnsubscribeTopicInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createUnsubscribeTopic"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}}]}}]}}]} as unknown as DocumentNode<CreateUnsubscribeTopicMutation, CreateUnsubscribeTopicMutationVariables>;
export const DeleteUnsubscribeTopicDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteUnsubscribeTopic"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteUnsubscribeTopic"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode<DeleteUnsubscribeTopicMutation, DeleteUnsubscribeTopicMutationVariables>;
export const UpdateUnsubscribeTopicDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUnsubscribeTopic"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUnsubscribeTopicInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUnsubscribeTopic"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}}]}}]}}]} as unknown as DocumentNode<UpdateUnsubscribeTopicMutation, UpdateUnsubscribeTopicMutationVariables>;
export const UnsubscribePagePreviewUrlDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UnsubscribePagePreviewUrl"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unsubscribePagePreviewUrl"}}]}}]} as unknown as DocumentNode<UnsubscribePagePreviewUrlQuery, UnsubscribePagePreviewUrlQueryVariables>;
export const MessageSuppressionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MessageSuppressions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FindMessageSuppressionsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"messageSuppressions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"emailAddress"}},{"kind":"Field","name":{"kind":"Name","value":"reason"}},{"kind":"Field","name":{"kind":"Name","value":"unsubscribeTopicId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode<MessageSuppressionsQuery, MessageSuppressionsQueryVariables>;
export const GetUsageAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUsageAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UsageAnalyticsInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getUsageAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"usageByUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"creditsUsed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"usageByOperationType"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"creditsUsed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"usageByModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"creditsUsed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"timeSeries"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"creditsUsed"}}]}},{"kind":"Field","name":{"kind":"Name","value":"periodStart"}},{"kind":"Field","name":{"kind":"Name","value":"periodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"userDailyUsage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"dailyUsage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"creditsUsed"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetUsageAnalyticsQuery, GetUsageAnalyticsQueryVariables>;
export const DeleteUserAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteUserAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
export const DeleteUserWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteUserWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceMemberIdToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteUserFromWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceMemberIdToDelete"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceMemberIdToDelete"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<DeleteUserWorkspaceMutation, DeleteUserWorkspaceMutationVariables>;
@@ -10,6 +10,7 @@ import { type MessageChannelVisibility } from '~/generated/graphql';
export type MessageChannel = {
id: string;
handle: string;
displayName: string | null;
visibility: MessageChannelVisibility;
type: MessageChannelType;
isContactAutoCreationEnabled: boolean;
@@ -214,6 +214,14 @@ const SettingsWorkspaceUnsubscribeTopicDetail = lazy(() =>
),
);
const SettingsWorkspaceUnsubscribe = lazy(() =>
import('~/pages/settings/communications/SettingsWorkspaceUnsubscribe').then(
(module) => ({
default: module.SettingsWorkspaceUnsubscribe,
}),
),
);
const SettingsSubdomainPage = lazy(() =>
import('~/pages/settings/domains/SettingsSubdomainPage').then((module) => ({
default: module.SettingsSubdomainPage,
@@ -724,6 +732,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
path={SettingsPath.UnsubscribeTopicDetail}
element={<SettingsWorkspaceUnsubscribeTopicDetail />}
/>
<Route
path={SettingsPath.Unsubscribe}
element={<SettingsWorkspaceUnsubscribe />}
/>
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
<Route
path={SettingsPath.BillingPlans}
@@ -20,12 +20,17 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
const { createEmailGroupChannel, loading } = useCreateEmailGroupChannel();
const [handle, setHandle] = useState('');
const [displayName, setDisplayName] = useState('');
const isHandleValidEmail = z.email().safeParse(handle).success;
const canSave = isHandleValidEmail && !loading;
const handleSave = useCallback(async () => {
const result = await createEmailGroupChannel(handle);
const trimmedDisplayName = displayName.trim();
const result = await createEmailGroupChannel(
handle,
trimmedDisplayName.length > 0 ? trimmedDisplayName : undefined,
);
const messageChannelId =
result.data?.createEmailGroupChannel.messageChannel.id;
@@ -34,7 +39,7 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
messageChannelId,
});
}
}, [createEmailGroupChannel, handle, navigate]);
}, [createEmailGroupChannel, displayName, handle, navigate]);
return (
<SettingsPageLayout
@@ -80,6 +85,26 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
disabled={loading}
/>
</Section>
<Section>
<H2Title
title={t`Display Name`}
description={t`The name recipients see next to your address in their inbox, instead of the address alone.`}
/>
<SettingsTextInput
instanceId="email-group-display-name"
label={t`Display Name`}
placeholder={t`Support Team`}
value={displayName}
maxLength={255}
onChange={setDisplayName}
onInputEnter={() => {
if (canSave) {
handleSave();
}
}}
disabled={loading}
/>
</Section>
</SettingsPageContainer>
</SettingsPageLayout>
);
@@ -0,0 +1,10 @@
import { gql } from '@apollo/client';
export const UPDATE_EMAIL_GROUP_CHANNEL = gql`
mutation UpdateEmailGroupChannel($input: UpdateEmailGroupChannelInput!) {
updateEmailGroupChannel(input: $input) {
id
displayName
}
}
`;
@@ -5,6 +5,7 @@ export const GET_MY_MESSAGE_CHANNELS = gql`
myMessageChannels(connectedAccountId: $connectedAccountId) {
id
handle
displayName
visibility
type
isContactAutoCreationEnabled
@@ -30,6 +30,7 @@ type CreateEmailGroupChannelResult = {
type CreateEmailGroupChannelVariables = {
input: {
handle: string;
displayName?: string;
};
};
@@ -47,9 +48,9 @@ export const useCreateEmailGroupChannel = () => {
],
});
const createEmailGroupChannel = (handle: string) =>
const createEmailGroupChannel = (handle: string, displayName?: string) =>
mutate({
variables: { input: { handle } },
variables: { input: { handle, displayName } },
onError: (mutationError) => {
enqueueErrorSnackBar({ apolloError: mutationError });
},
@@ -0,0 +1,32 @@
import { useMutation } from '@apollo/client/react';
import { UPDATE_EMAIL_GROUP_CHANNEL } from '@/settings/accounts/graphql/mutations/updateEmailGroupChannel';
import { GET_MY_MESSAGE_CHANNELS } from '@/settings/accounts/graphql/queries/getMyMessageChannels';
type UpdateEmailGroupChannelResult = {
updateEmailGroupChannel: {
id: string;
displayName: string | null;
};
};
type UpdateEmailGroupChannelVariables = {
input: {
id: string;
displayName?: string | null;
};
};
export const useUpdateEmailGroupChannel = () => {
const [mutate, { loading, error }] = useMutation<
UpdateEmailGroupChannelResult,
UpdateEmailGroupChannelVariables
>(UPDATE_EMAIL_GROUP_CHANNEL, {
refetchQueries: [{ query: GET_MY_MESSAGE_CHANNELS }],
});
const updateEmailGroupChannel = (id: string, displayName: string | null) =>
mutate({ variables: { input: { id, displayName } } });
return { updateEmailGroupChannel, loading, error };
};
@@ -0,0 +1,55 @@
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { Button } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type SettingsPaginationControlsProps = {
page: number;
pageCount: number;
onPageChange: (page: number) => void;
isLoading?: boolean;
hasNextPage?: boolean;
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
justify-content: flex-end;
padding-top: ${themeCssVariables.spacing[2]};
`;
export const SettingsPaginationControls = ({
page,
pageCount,
onPageChange,
isLoading = false,
hasNextPage,
}: SettingsPaginationControlsProps) => {
const { t } = useLingui();
const isNextDisabled = isDefined(hasNextPage)
? !hasNextPage
: page + 1 >= pageCount;
return (
<StyledContainer>
<Button
title={t`Previous`}
variant="secondary"
size="small"
disabled={page === 0 || isLoading}
onClick={() => onPageChange(page - 1)}
/>
<div>{t`Page ${page + 1} of ${pageCount}`}</div>
<Button
title={t`Next`}
variant="secondary"
size="small"
disabled={isNextDisabled || isLoading}
onClick={() => onPageChange(page + 1)}
/>
</StyledContainer>
);
};
@@ -1,5 +1,7 @@
import { type ComponentType, type ReactNode } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { styled } from '@linaria/react';
import { Table } from '@/ui/layout/table/components/Table';
@@ -50,13 +52,14 @@ type SettingsTableListSectionProps<Item extends { id: string }> = {
title: string;
description: string;
headerAdornment?: ReactNode;
toolbar?: ReactNode;
items: Item[];
columns: SettingsTableListSectionColumn<Item>[];
gridAutoColumns: string;
showRowChevron?: boolean;
onRowClick?: (item: Item) => void;
footerButtonLabel: string;
onFooterButtonClick: () => void;
footerButtonLabel?: string;
onFooterButtonClick?: () => void;
};
export const SettingsTableListSection = <
@@ -65,6 +68,7 @@ export const SettingsTableListSection = <
title,
description,
headerAdornment,
toolbar,
items,
columns,
gridAutoColumns,
@@ -84,6 +88,7 @@ export const SettingsTableListSection = <
description={description}
adornment={headerAdornment}
/>
{isDefined(toolbar) && toolbar}
{items.length > 0 && (
<Table>
<TableRow gridAutoColumns={resolvedGridAutoColumns}>
@@ -124,15 +129,17 @@ export const SettingsTableListSection = <
</StyledTableRows>
</Table>
)}
<StyledFooter>
<Button
Icon={IconPlus}
title={footerButtonLabel}
variant="secondary"
size="small"
onClick={onFooterButtonClick}
/>
</StyledFooter>
{isDefined(footerButtonLabel) && isDefined(onFooterButtonClick) && (
<StyledFooter>
<Button
Icon={IconPlus}
title={footerButtonLabel}
variant="secondary"
size="small"
onClick={onFooterButtonClick}
/>
</StyledFooter>
)}
</Section>
);
};
@@ -1,7 +0,0 @@
import { gql } from '@apollo/client';
export const GET_UNSUBSCRIBE_PAGE_PREVIEW_URL = gql`
query UnsubscribePagePreviewUrl {
unsubscribePagePreviewUrl
}
`;
@@ -0,0 +1,107 @@
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useUnsubscribeTopics } from '@/activities/emails/hooks/useUnsubscribeTopics';
import { Button, Checkbox } from 'twenty-ui/input';
import { HorizontalSeparator, Section } from 'twenty-ui/layout';
import { Card } from 'twenty-ui/surfaces';
import { H2Title } from 'twenty-ui/typography';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { UnsubscribeTopicVisibility } from '~/generated-metadata/graphql';
const StyledViewport = styled.div`
align-items: center;
background: ${themeCssVariables.background.secondary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.md};
display: flex;
justify-content: center;
padding: ${themeCssVariables.spacing[10]} ${themeCssVariables.spacing[6]};
`;
const StyledCard = styled(Card)`
--card-background-color: ${themeCssVariables.background.primary};
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[5]};
max-width: 420px;
padding: ${themeCssVariables.spacing[8]};
width: 100%;
`;
const StyledHeader = styled.div`
text-align: center;
h2 {
font-size: 20px;
}
`;
const StyledTopics = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[3]};
`;
const StyledTopicRow = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.primary};
display: flex;
font-size: ${themeCssVariables.font.size.md};
gap: ${themeCssVariables.spacing[2]};
`;
export const SettingsUnsubscribePreview = () => {
const { t } = useLingui();
const { unsubscribeTopics } = useUnsubscribeTopics();
const publicTopics = unsubscribeTopics.filter(
(topic) => topic.visibility === UnsubscribeTopicVisibility.PUBLIC,
);
return (
<Section>
<H2Title
title={t`Unsubscribe page`}
description={t`Preview of the page recipients see when they unsubscribe`}
/>
<StyledViewport>
<StyledCard rounded>
<StyledHeader>
<H2Title
title={t`Do you want to unsubscribe?`}
description={t`Confirm your preferences:`}
/>
</StyledHeader>
<StyledTopics>
{publicTopics.map((topic) => (
<StyledTopicRow key={topic.id}>
<Checkbox
checked
onChange={() => {}}
aria-label={topic.name ?? t`Untitled topic`}
/>
{topic.name ?? t`Untitled topic`}
</StyledTopicRow>
))}
</StyledTopics>
<Button
title={t`Update`}
variant="primary"
accent="blue"
fullWidth
justify="center"
/>
<HorizontalSeparator text={t`Or`} noMargin />
<Button
title={t`Unsubscribe all`}
variant="secondary"
fullWidth
justify="center"
/>
</StyledCard>
</StyledViewport>
</Section>
);
};
@@ -0,0 +1,196 @@
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useDebounce } from 'use-debounce';
import { isDefined } from 'twenty-shared/utils';
import { useUnsubscribeTopics } from '@/activities/emails/hooks/useUnsubscribeTopics';
import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder';
import { SettingsPaginationControls } from '@/settings/components/SettingsPaginationControls';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { SettingsTableListSection } from '@/settings/components/SettingsTableListSection';
import { SettingsUnsubscribersFilterDropdown } from '@/settings/unsubscribers/components/filter-dropdown/SettingsUnsubscribersFilterDropdown';
import { SETTINGS_UNSUBSCRIBERS_ALL_FILTER } from '@/settings/unsubscribers/constants/SettingsUnsubscribersAllFilter';
import { MESSAGE_SUPPRESSIONS_PAGE_SIZE } from '@/settings/unsubscribers/constants/MessageSuppressionsPageSize';
import { useMessageSuppressions } from '@/settings/unsubscribers/hooks/useMessageSuppressions';
import { getMessageSuppressionReasonBadge } from '@/settings/unsubscribers/utils/getMessageSuppressionReasonBadge';
import { Status } from 'twenty-ui/data-display';
import { SearchInput } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
MessageSuppressionReason,
type MessageSuppressionsQuery,
} from '~/generated-metadata/graphql';
import { formatToHumanReadableDate } from '~/utils/date-utils';
type MessageSuppression =
MessageSuppressionsQuery['messageSuppressions']['records'][number];
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[4]};
`;
const StyledToolbar = styled.div`
display: flex;
gap: ${themeCssVariables.spacing[2]};
padding-bottom: ${themeCssVariables.spacing[2]};
width: 100%;
`;
const StyledSearch = styled.div`
flex: 1;
`;
export const SettingsUnsubscribersList = () => {
const { t } = useLingui();
const [page, setPage] = useState(0);
const [searchText, setSearchText] = useState('');
const [debouncedSearchText] = useDebounce(searchText, 300);
const [reasonFilter, setReasonFilter] = useState(
SETTINGS_UNSUBSCRIBERS_ALL_FILTER,
);
const [topicFilter, setTopicFilter] = useState(
SETTINGS_UNSUBSCRIBERS_ALL_FILTER,
);
const { unsubscribeTopics } = useUnsubscribeTopics();
const reason = Object.values(MessageSuppressionReason).find(
(reasonValue) => reasonValue === reasonFilter,
);
const unsubscribeTopicId =
topicFilter === SETTINGS_UNSUBSCRIBERS_ALL_FILTER ? undefined : topicFilter;
const { messageSuppressions, totalCount, loading } = useMessageSuppressions({
page,
searchTerm: debouncedSearchText,
reason,
unsubscribeTopicId,
});
const goToFirstPage = () => setPage(0);
const handleSearchChange = (value: string) => {
setSearchText(value);
goToFirstPage();
};
const handleReasonChange = (value: string) => {
setReasonFilter(value);
goToFirstPage();
};
const handleTopicChange = (value: string) => {
setTopicFilter(value);
goToFirstPage();
};
const handleClearFilters = () => {
setReasonFilter(SETTINGS_UNSUBSCRIBERS_ALL_FILTER);
setTopicFilter(SETTINGS_UNSUBSCRIBERS_ALL_FILTER);
goToFirstPage();
};
const topicNameById = new Map(
unsubscribeTopics.map((topic) => [
topic.id,
topic.name ?? t`Untitled topic`,
]),
);
const getScopeLabel = (topicId: string | null) => {
if (!isDefined(topicId)) {
return t`All emails`;
}
return topicNameById.get(topicId) ?? t`Unknown topic`;
};
const items = loading ? [] : messageSuppressions;
const isEmpty = !loading && items.length === 0;
const hasPagination = totalCount > MESSAGE_SUPPRESSIONS_PAGE_SIZE;
const pageCount = Math.max(
1,
Math.ceil(totalCount / MESSAGE_SUPPRESSIONS_PAGE_SIZE),
);
return (
<StyledContainer>
<SettingsTableListSection<MessageSuppression>
title={t`Unsubscribers`}
description={t`Email addresses that will no longer receive campaign emails`}
toolbar={
<StyledToolbar>
<StyledSearch>
<SearchInput
placeholder={t`Search by email address`}
value={searchText}
onChange={handleSearchChange}
/>
</StyledSearch>
<SettingsUnsubscribersFilterDropdown
topics={unsubscribeTopics}
reasonValue={reasonFilter}
topicValue={topicFilter}
onChangeReason={handleReasonChange}
onChangeTopic={handleTopicChange}
onClear={handleClearFilters}
/>
</StyledToolbar>
}
items={items}
columns={[
{
label: t`Email address`,
Cell: ({ item }) => <>{item.emailAddress}</>,
},
{
label: t`Scope`,
Cell: ({ item }) => (
<>{getScopeLabel(item.unsubscribeTopicId ?? null)}</>
),
},
{
label: t`Reason`,
Cell: ({ item }) => {
const badge = getMessageSuppressionReasonBadge(item.reason);
return (
<Status
color={badge.color}
text={badge.label}
weight="medium"
/>
);
},
},
{
label: t`Date`,
align: 'right',
Cell: ({ item }) => (
<>{formatToHumanReadableDate(item.createdAt)}</>
),
},
]}
gridAutoColumns="1fr 160px 140px 160px"
/>
{loading && <SettingsSectionSkeletonLoader />}
{isEmpty && (
<SettingsEmptyPlaceholder>
{t`No unsubscribers found`}
</SettingsEmptyPlaceholder>
)}
{hasPagination && (
<SettingsPaginationControls
page={page}
pageCount={pageCount}
onPageChange={setPage}
isLoading={loading}
/>
)}
</StyledContainer>
);
};
@@ -0,0 +1,123 @@
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { IconFilter } from 'twenty-ui/icon';
import { Button } from 'twenty-ui/input';
import { SettingsUnsubscribersFilterMenuContent } from '@/settings/unsubscribers/components/filter-dropdown/SettingsUnsubscribersFilterMenuContent';
import { SettingsUnsubscribersFilterOptionsContent } from '@/settings/unsubscribers/components/filter-dropdown/SettingsUnsubscribersFilterOptionsContent';
import { type SettingsUnsubscribersFilterContentId } from '@/settings/unsubscribers/components/filter-dropdown/types/SettingsUnsubscribersFilterContentId';
import { type SettingsUnsubscribersFilterOption } from '@/settings/unsubscribers/components/filter-dropdown/types/SettingsUnsubscribersFilterOption';
import { SETTINGS_UNSUBSCRIBERS_ALL_FILTER } from '@/settings/unsubscribers/constants/SettingsUnsubscribersAllFilter';
import { getMessageSuppressionReasonBadge } from '@/settings/unsubscribers/utils/getMessageSuppressionReasonBadge';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import {
MessageSuppressionReason,
type UnsubscribeTopicsQuery,
} from '~/generated-metadata/graphql';
const SETTINGS_UNSUBSCRIBERS_FILTER_DROPDOWN_ID =
'settings-unsubscribers-filter-dropdown';
const getOptionLabel = (
options: SettingsUnsubscribersFilterOption[],
value: string,
) => options.find((option) => option.value === value)?.label ?? '';
type SettingsUnsubscribersFilterDropdownProps = {
topics: UnsubscribeTopicsQuery['unsubscribeTopics'];
reasonValue: string;
topicValue: string;
onChangeReason: (value: string) => void;
onChangeTopic: (value: string) => void;
onClear: () => void;
};
export const SettingsUnsubscribersFilterDropdown = ({
topics,
reasonValue,
topicValue,
onChangeReason,
onChangeTopic,
onClear,
}: SettingsUnsubscribersFilterDropdownProps) => {
const { t } = useLingui();
const [contentId, setContentId] =
useState<SettingsUnsubscribersFilterContentId | null>(null);
const goToMenu = () => setContentId(null);
const reasonOptions: SettingsUnsubscribersFilterOption[] = [
{ value: SETTINGS_UNSUBSCRIBERS_ALL_FILTER, label: t`All reasons` },
...Object.values(MessageSuppressionReason).map((reason) => ({
value: reason,
label: getMessageSuppressionReasonBadge(reason).label,
})),
];
const topicOptions: SettingsUnsubscribersFilterOption[] = [
{ value: SETTINGS_UNSUBSCRIBERS_ALL_FILTER, label: t`All topics` },
...topics.map((topic) => ({
value: topic.id,
label: topic.name ?? t`Untitled topic`,
})),
];
const hasActiveFilters =
reasonValue !== SETTINGS_UNSUBSCRIBERS_ALL_FILTER ||
topicValue !== SETTINGS_UNSUBSCRIBERS_ALL_FILTER;
const renderContent = () => {
switch (contentId) {
case 'reason':
return (
<SettingsUnsubscribersFilterOptionsContent
title={t`Reason`}
options={reasonOptions}
selectedValue={reasonValue}
onSelect={onChangeReason}
onBack={goToMenu}
/>
);
case 'topic':
return (
<SettingsUnsubscribersFilterOptionsContent
title={t`Topic`}
options={topicOptions}
selectedValue={topicValue}
onSelect={onChangeTopic}
onBack={goToMenu}
/>
);
default:
return (
<SettingsUnsubscribersFilterMenuContent
reasonLabel={getOptionLabel(reasonOptions, reasonValue)}
topicLabel={getOptionLabel(topicOptions, topicValue)}
hasActiveFilters={hasActiveFilters}
onContentChange={setContentId}
onClear={onClear}
/>
);
}
};
return (
<Dropdown
dropdownId={SETTINGS_UNSUBSCRIBERS_FILTER_DROPDOWN_ID}
dropdownPlacement="bottom-end"
dropdownOffset={{ x: 0, y: 8 }}
onClose={goToMenu}
clickableComponent={
<Button
Icon={IconFilter}
size="medium"
variant="secondary"
accent="default"
ariaLabel={t`Filter unsubscribers`}
/>
}
dropdownComponents={renderContent()}
/>
);
};
@@ -0,0 +1,60 @@
import { useLingui } from '@lingui/react/macro';
import { IconMailCog, IconStatusChange, IconTrash } from 'twenty-ui/icon';
import { MenuItem } from 'twenty-ui/navigation';
import { type SettingsUnsubscribersFilterContentId } from '@/settings/unsubscribers/components/filter-dropdown/types/SettingsUnsubscribersFilterContentId';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
type SettingsUnsubscribersFilterMenuContentProps = {
reasonLabel: string;
topicLabel: string;
hasActiveFilters: boolean;
onContentChange: (contentId: SettingsUnsubscribersFilterContentId) => void;
onClear: () => void;
};
export const SettingsUnsubscribersFilterMenuContent = ({
reasonLabel,
topicLabel,
hasActiveFilters,
onContentChange,
onClear,
}: SettingsUnsubscribersFilterMenuContentProps) => {
const { t } = useLingui();
return (
<DropdownContent>
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconStatusChange}
text={t`Reason`}
contextualText={reasonLabel}
contextualTextPosition="right"
hasSubMenu
onClick={() => onContentChange('reason')}
/>
<MenuItem
LeftIcon={IconMailCog}
text={t`Topic`}
contextualText={topicLabel}
contextualTextPosition="right"
hasSubMenu
onClick={() => onContentChange('topic')}
/>
{hasActiveFilters && (
<>
<DropdownMenuSeparator />
<MenuItem
accent="danger"
LeftIcon={IconTrash}
text={t`Clear filters`}
onClick={onClear}
/>
</>
)}
</DropdownMenuItemsContainer>
</DropdownContent>
);
};
@@ -0,0 +1,57 @@
import { IconChevronLeft } from 'twenty-ui/icon';
import { MenuItemSelect } from 'twenty-ui/navigation';
import { type SettingsUnsubscribersFilterOption } from '@/settings/unsubscribers/components/filter-dropdown/types/SettingsUnsubscribersFilterOption';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
type SettingsUnsubscribersFilterOptionsContentProps = {
title: string;
options: SettingsUnsubscribersFilterOption[];
selectedValue: string;
onSelect: (value: string) => void;
onBack: () => void;
};
export const SettingsUnsubscribersFilterOptionsContent = ({
title,
options,
selectedValue,
onSelect,
onBack,
}: SettingsUnsubscribersFilterOptionsContentProps) => {
const { closeDropdown } = useCloseDropdown();
const handleSelect = (value: string) => {
onSelect(value);
closeDropdown();
};
return (
<DropdownContent>
<DropdownMenuHeader
StartComponent={
<DropdownMenuHeaderLeftComponent
onClick={onBack}
Icon={IconChevronLeft}
/>
}
>
{title}
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
{options.map((option) => (
<MenuItemSelect
key={option.value}
text={option.label}
selected={selectedValue === option.value}
onClick={() => handleSelect(option.value)}
/>
))}
</DropdownMenuItemsContainer>
</DropdownContent>
);
};
@@ -0,0 +1 @@
export type SettingsUnsubscribersFilterContentId = 'reason' | 'topic';
@@ -0,0 +1,4 @@
export type SettingsUnsubscribersFilterOption = {
value: string;
label: string;
};
@@ -0,0 +1 @@
export const MESSAGE_SUPPRESSIONS_PAGE_SIZE = 30;
@@ -0,0 +1,5 @@
export const SETTINGS_UNSUBSCRIBE_TAB_IDS = {
UNSUBSCRIBERS: 'unsubscribers',
TOPICS: 'topics',
PREVIEW: 'preview',
} as const;
@@ -0,0 +1 @@
export const SETTINGS_UNSUBSCRIBERS_ALL_FILTER = 'all';
@@ -0,0 +1,16 @@
import { gql } from '@apollo/client';
export const MESSAGE_SUPPRESSIONS = gql`
query MessageSuppressions($input: FindMessageSuppressionsInput!) {
messageSuppressions(input: $input) {
records {
id
createdAt
emailAddress
reason
unsubscribeTopicId
}
totalCount
}
}
`;
@@ -0,0 +1,43 @@
import { useQuery } from '@apollo/client/react';
import { isNonEmptyString } from '@sniptt/guards';
import { MESSAGE_SUPPRESSIONS_PAGE_SIZE } from '@/settings/unsubscribers/constants/MessageSuppressionsPageSize';
import {
type MessageSuppressionReason,
MessageSuppressionsDocument,
} from '~/generated-metadata/graphql';
type UseMessageSuppressionsParams = {
page?: number;
searchTerm?: string;
reason?: MessageSuppressionReason;
unsubscribeTopicId?: string;
skip?: boolean;
};
export const useMessageSuppressions = ({
page = 0,
searchTerm,
reason,
unsubscribeTopicId,
skip,
}: UseMessageSuppressionsParams) => {
const { data, loading } = useQuery(MessageSuppressionsDocument, {
variables: {
input: {
limit: MESSAGE_SUPPRESSIONS_PAGE_SIZE,
offset: page * MESSAGE_SUPPRESSIONS_PAGE_SIZE,
...(isNonEmptyString(searchTerm) ? { searchTerm } : {}),
...(isNonEmptyString(unsubscribeTopicId) ? { unsubscribeTopicId } : {}),
...(reason ? { reason } : {}),
},
},
skip,
});
return {
messageSuppressions: data?.messageSuppressions.records ?? [],
totalCount: data?.messageSuppressions.totalCount ?? 0,
loading,
};
};
@@ -0,0 +1,22 @@
import { getMessageSuppressionReasonBadge } from '@/settings/unsubscribers/utils/getMessageSuppressionReasonBadge';
import { MessageSuppressionReason } from '~/generated-metadata/graphql';
describe('getMessageSuppressionReasonBadge', () => {
it('should badge unsubscribes as gray', () => {
expect(
getMessageSuppressionReasonBadge(MessageSuppressionReason.UNSUBSCRIBE),
).toEqual({ color: 'gray', label: 'Unsubscribed' });
});
it('should badge complaints as red', () => {
expect(
getMessageSuppressionReasonBadge(MessageSuppressionReason.COMPLAINT),
).toEqual({ color: 'red', label: 'Complaint' });
});
it('should badge bounces as orange', () => {
expect(
getMessageSuppressionReasonBadge(MessageSuppressionReason.BOUNCE),
).toEqual({ color: 'orange', label: 'Bounce' });
});
});
@@ -0,0 +1,23 @@
import { t } from '@lingui/core/macro';
import { type ThemeColor } from 'twenty-ui/theme';
import { MessageSuppressionReason } from '~/generated-metadata/graphql';
type MessageSuppressionReasonBadge = {
color: ThemeColor;
label: string;
};
export const getMessageSuppressionReasonBadge = (
reason: MessageSuppressionReason,
): MessageSuppressionReasonBadge => {
switch (reason) {
case MessageSuppressionReason.UNSUBSCRIBE:
return { color: 'gray', label: t`Unsubscribed` };
case MessageSuppressionReason.COMPLAINT:
return { color: 'red', label: t`Complaint` };
case MessageSuppressionReason.BOUNCE:
return { color: 'orange', label: t`Bounce` };
default:
return { color: 'gray', label: t`Unknown` };
}
};
@@ -1,10 +1,13 @@
import { useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useDeleteEmailGroupChannel } from '@/settings/accounts/hooks/useDeleteEmailGroupChannel';
import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels';
import { useUpdateEmailGroupChannel } from '@/settings/accounts/hooks/useUpdateEmailGroupChannel';
import { SettingsEditableTitle } from '@/settings/components/SettingsEditableTitle';
import { getEmailChannelDomain } from '@/settings/accounts/utils/getEmailChannelDomain';
import { SettingsDnsRecordsTable } from '@/settings/components/SettingsDnsRecordsTable';
@@ -17,6 +20,7 @@ import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { isNonEmptyString } from '@sniptt/guards';
import { MessageChannelType, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import {
@@ -70,8 +74,12 @@ export const SettingsWorkspaceCommunicationGroupChannelDetail = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const { deleteEmailGroupChannel, loading: deleting } =
useDeleteEmailGroupChannel();
const { updateEmailGroupChannel, loading: updatingDisplayName } =
useUpdateEmailGroupChannel();
const { data: emailingDomainsData } = useQuery(GetEmailingDomainsDocument);
const [displayNameDraft, setDisplayNameDraft] = useState<string | null>(null);
if (loading) {
return <SettingsSkeletonLoader />;
}
@@ -118,6 +126,31 @@ export const SettingsWorkspaceCommunicationGroupChannelDetail = () => {
? EmailingDomainStatus.FAILED
: EmailingDomainStatus.PENDING;
const displayName = channel.displayName ?? '';
const handleDisplayNameSave = async () => {
if (!isDefined(displayNameDraft) || displayNameDraft === displayName) {
setDisplayNameDraft(null);
return;
}
const nextDisplayName = displayNameDraft.trim();
try {
await updateEmailGroupChannel(
channel.id,
isNonEmptyString(nextDisplayName) ? nextDisplayName : null,
);
} catch {
enqueueErrorSnackBar({
message: t`Failed to update sender name.`,
});
} finally {
setDisplayNameDraft(null);
}
};
const handleDelete = async () => {
try {
await deleteEmailGroupChannel(channel.id);
@@ -131,7 +164,19 @@ export const SettingsWorkspaceCommunicationGroupChannelDetail = () => {
return (
<SettingsPageLayout
title={sourceHandle}
title={
<SettingsEditableTitle
instanceId="email-group-display-name"
value={displayNameDraft ?? displayName}
placeholder={t`Sender name`}
disabled={updatingDisplayName}
onChange={setDisplayNameDraft}
onEnter={handleDisplayNameSave}
onTab={handleDisplayNameSave}
onClickOutside={handleDisplayNameSave}
onEscape={() => setDisplayNameDraft(null)}
/>
}
links={[
{
children: t`Workspace`,
@@ -141,6 +186,7 @@ export const SettingsWorkspaceCommunicationGroupChannelDetail = () => {
children: t`Communication`,
href: getSettingsPath(SettingsPath.WorkspaceCommunications),
},
{ children: sourceHandle },
]}
actionButton={
<Button
@@ -1,11 +1,8 @@
import { useApolloClient, useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { GET_UNSUBSCRIBE_PAGE_PREVIEW_URL } from '@/settings/unsubscribe-topics/graphql/queries/getUnsubscribePagePreviewUrl';
import { SettingsWorkspaceUnsubscribeTopicSection } from '@/settings/unsubscribe-topics/components/SettingsWorkspaceUnsubscribeTopicSection';
import { SettingsWorkspaceEmailGroupSection } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupSection';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
@@ -24,14 +21,15 @@ import coverDark from '~/pages/settings/communications/assets/cover-dark.png';
import coverLight from '~/pages/settings/communications/assets/cover-light.png';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { useContext } from 'react';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const COMMUNICATIONS_TABS_INSTANCE_ID = 'settings-communications-tabs';
const StyledCardLink = styled.a`
display: block;
min-width: 0;
text-decoration: none;
const StyledCardsColumn = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
`;
export const SettingsWorkspaceCommunications = () => {
@@ -39,21 +37,12 @@ export const SettingsWorkspaceCommunications = () => {
const { t } = useLingui();
const navigateSettings = useNavigateSettings();
const isEmailGroupFeatureEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_EMAIL_GROUP_ENABLED,
);
const apolloClient = useApolloClient();
const { data: unsubscribePreviewData } = useQuery<{
unsubscribePagePreviewUrl: string;
}>(GET_UNSUBSCRIBE_PAGE_PREVIEW_URL, {
client: apolloClient,
skip: !isEmailGroupFeatureEnabled,
});
const unsubscribePageUrl = unsubscribePreviewData?.unsubscribePagePreviewUrl;
if (!isEmailGroupFeatureEnabled) {
return null;
}
@@ -99,17 +88,12 @@ export const SettingsWorkspaceCommunications = () => {
/>
</Section>
<SettingsWorkspaceEmailGroupSection />
<SettingsWorkspaceUnsubscribeTopicSection />
<Section>
<H2Title
title={t`Unsubscribe`}
description={t`The page your users will get redirected to to unsubscribe from your emails`}
description={t`Manage unsubscribers, opt-out topics, and the page recipients see`}
/>
<StyledCardLink
href={unsubscribePageUrl}
target="_blank"
rel="noopener noreferrer"
>
<StyledCardsColumn>
<SettingsCard
Icon={
<IconMailX
@@ -117,9 +101,10 @@ export const SettingsWorkspaceCommunications = () => {
stroke={theme.icon.stroke.md}
/>
}
title={t`See unsubscribe page`}
title={t`Manage unsubscribe`}
onClick={() => navigateSettings(SettingsPath.Unsubscribe)}
/>
</StyledCardLink>
</StyledCardsColumn>
</Section>
</SettingsPageContainer>
</SettingsPageLayout>
@@ -2,6 +2,7 @@ import { useLingui } from '@lingui/react/macro';
import { useCallback, useState } from 'react';
import { useCreateUnsubscribeTopic } from '@/settings/unsubscribe-topics/hooks/useCreateUnsubscribeTopic';
import { SETTINGS_UNSUBSCRIBE_TAB_IDS } from '@/settings/unsubscribers/constants/SettingsUnsubscribeTabIds';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
@@ -34,6 +35,18 @@ export const SettingsWorkspaceNewUnsubscribeTopic = () => {
const canSave = name.length > 0 && !loading;
const navigateToTopics = useCallback(
() =>
navigate(
SettingsPath.Unsubscribe,
undefined,
undefined,
undefined,
SETTINGS_UNSUBSCRIBE_TAB_IDS.TOPICS,
),
[navigate],
);
const handleSave = useCallback(async () => {
try {
const result = await createUnsubscribeTopic({
@@ -50,7 +63,7 @@ export const SettingsWorkspaceNewUnsubscribeTopic = () => {
unsubscribeTopicId,
});
} else {
navigate(SettingsPath.WorkspaceCommunications);
navigateToTopics();
}
} catch {
enqueueErrorSnackBar({
@@ -63,6 +76,7 @@ export const SettingsWorkspaceNewUnsubscribeTopic = () => {
description,
isPublic,
navigate,
navigateToTopics,
enqueueErrorSnackBar,
t,
]);
@@ -83,6 +97,15 @@ export const SettingsWorkspaceNewUnsubscribeTopic = () => {
children: t`Communication`,
href: getSettingsPath(SettingsPath.WorkspaceCommunications),
},
{
children: t`Unsubscribe`,
href: getSettingsPath(
SettingsPath.Unsubscribe,
undefined,
undefined,
SETTINGS_UNSUBSCRIBE_TAB_IDS.TOPICS,
),
},
{ children: t`New Unsubscribe Topic` },
]}
actionButton={
@@ -90,7 +113,7 @@ export const SettingsWorkspaceNewUnsubscribeTopic = () => {
isSaveDisabled={!canSave}
isCancelDisabled={loading}
isLoading={loading}
onCancel={() => navigate(SettingsPath.WorkspaceCommunications)}
onCancel={navigateToTopics}
onSave={handleSave}
/>
}
@@ -0,0 +1,84 @@
import { useLingui } from '@lingui/react/macro';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useSettingsActiveTabId } from '@/settings/components/layout/useSettingsActiveTabId';
import { SettingsWorkspaceUnsubscribeTopicSection } from '@/settings/unsubscribe-topics/components/SettingsWorkspaceUnsubscribeTopicSection';
import { SettingsUnsubscribePreview } from '@/settings/unsubscribers/components/SettingsUnsubscribePreview';
import { SettingsUnsubscribersList } from '@/settings/unsubscribers/components/SettingsUnsubscribersList';
import { SETTINGS_UNSUBSCRIBE_TAB_IDS } from '@/settings/unsubscribers/constants/SettingsUnsubscribeTabIds';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconEye, IconForbid, IconMailCog } from 'twenty-ui/icon';
const UNSUBSCRIBE_TABS_INSTANCE_ID = 'settings-unsubscribe-tabs';
export const SettingsWorkspaceUnsubscribe = () => {
const { t } = useLingui();
const isEmailGroupEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_EMAIL_GROUP_ENABLED,
);
const tabs = [
{
id: SETTINGS_UNSUBSCRIBE_TAB_IDS.UNSUBSCRIBERS,
title: t`Unsubscribers`,
Icon: IconForbid,
},
{
id: SETTINGS_UNSUBSCRIBE_TAB_IDS.TOPICS,
title: t`Topics`,
Icon: IconMailCog,
},
{
id: SETTINGS_UNSUBSCRIBE_TAB_IDS.PREVIEW,
title: t`Unsubscribe page`,
Icon: IconEye,
},
];
const activeTabId = useSettingsActiveTabId(
UNSUBSCRIBE_TABS_INSTANCE_ID,
tabs.map((tab) => tab.id),
);
if (!isEmailGroupEnabled) {
return null;
}
return (
<SettingsPageLayout
title={t`Unsubscribe`}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.General),
},
{
children: t`Communication`,
href: getSettingsPath(SettingsPath.WorkspaceCommunications),
},
{ children: t`Unsubscribe` },
]}
>
<SettingsPageContainer>
<TabList
tabs={tabs}
componentInstanceId={UNSUBSCRIBE_TABS_INSTANCE_ID}
/>
{activeTabId === SETTINGS_UNSUBSCRIBE_TAB_IDS.UNSUBSCRIBERS && (
<SettingsUnsubscribersList />
)}
{activeTabId === SETTINGS_UNSUBSCRIBE_TAB_IDS.TOPICS && (
<SettingsWorkspaceUnsubscribeTopicSection />
)}
{activeTabId === SETTINGS_UNSUBSCRIBE_TAB_IDS.PREVIEW && (
<SettingsUnsubscribePreview />
)}
</SettingsPageContainer>
</SettingsPageLayout>
);
};
@@ -10,6 +10,7 @@ import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsO
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useDeleteUnsubscribeTopic } from '@/settings/unsubscribe-topics/hooks/useDeleteUnsubscribeTopic';
import { useUpdateUnsubscribeTopic } from '@/settings/unsubscribe-topics/hooks/useUpdateUnsubscribeTopic';
import { SETTINGS_UNSUBSCRIBE_TAB_IDS } from '@/settings/unsubscribers/constants/SettingsUnsubscribeTabIds';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
@@ -104,7 +105,13 @@ export const SettingsWorkspaceUnsubscribeTopicDetail = () => {
const handleDelete = async () => {
try {
await deleteUnsubscribeTopic(unsubscribeTopic.id);
navigateSettings(SettingsPath.WorkspaceCommunications);
navigateSettings(
SettingsPath.Unsubscribe,
undefined,
undefined,
undefined,
SETTINGS_UNSUBSCRIBE_TAB_IDS.TOPICS,
);
} catch {
enqueueErrorSnackBar({
message: t`Failed to delete unsubscribe topic.`,
@@ -126,6 +133,15 @@ export const SettingsWorkspaceUnsubscribeTopicDetail = () => {
children: t`Communication`,
href: getSettingsPath(SettingsPath.WorkspaceCommunications),
},
{
children: t`Unsubscribe`,
href: getSettingsPath(
SettingsPath.Unsubscribe,
undefined,
undefined,
SETTINGS_UNSUBSCRIBE_TAB_IDS.TOPICS,
),
},
{ children: topicName },
]}
actionButton={
@@ -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.25.0', 1784904030252)
export class AddMessageChannelDisplayNameFastInstanceCommand
implements FastInstanceCommand
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."messageChannel" ADD COLUMN IF NOT EXISTS "displayName" character varying',
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
'ALTER TABLE "core"."messageChannel" DROP COLUMN IF EXISTS "displayName"',
);
}
}
@@ -118,6 +118,7 @@ import { AddKanbanAndCalendarWidgetViewTypesFastInstanceCommand } from './2-23/2
import { WidenViewCalendarIntegrityConstraintFastInstanceCommand } from './2-23/2-23-instance-command-fast-1784620626406-widen-view-calendar-integrity-constraint';
import { AddApplicationIdToKeyValuePairFastInstanceCommand } from './2-23/2-23-instance-command-fast-1784659343818-add-application-id-to-key-value-pair';
import { AddLogoFileIdToApplicationRegistration2_23FastInstanceCommand } from './2-23/2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration';
import { AddMessageChannelDisplayNameFastInstanceCommand } from './2-25/2-25-instance-command-fast-1784904030252-add-message-channel-display-name';
import { BackfillCreatedWorkspaceActivationStatusSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784286705000-backfill-created-workspace-activation-status';
import { UnlistUnclaimedNpmApplicationRegistrationsSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784322591746-unlist-unclaimed-npm-application-registrations';
import { AddStatusesToBillingSubscriptionIndexSlowInstanceCommand } from './2-23/2-23-instance-command-slow-1784650048045-add-statuses-to-billing-subscription-index';
@@ -247,6 +248,7 @@ export const INSTANCE_COMMANDS = [
AddApplicationIdToKeyValuePairFastInstanceCommand,
AddSdkClientCoreChecksumToApplicationFastInstanceCommand,
AddLogoFileIdToApplicationRegistration2_23FastInstanceCommand,
AddMessageChannelDisplayNameFastInstanceCommand,
AddStatusesToBillingSubscriptionIndexSlowInstanceCommand,
AddOnConnectLogicFunctionToConnectionProviderFastInstanceCommand,
RepairKeyValuePairApplicationIdFastInstanceCommand,
@@ -0,0 +1,40 @@
import { Field, InputType, Int } from '@nestjs/graphql';
import {
IsEnum,
IsOptional,
IsString,
IsUUID,
Max,
Min,
} from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { MessageSuppressionReason } from 'src/engine/core-modules/emailing-domain/types/message-suppression-reason.type';
@InputType()
export class FindMessageSuppressionsInput {
@Field(() => MessageSuppressionReason, { nullable: true })
@IsOptional()
@IsEnum(MessageSuppressionReason)
reason?: MessageSuppressionReason;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
searchTerm?: string;
@Field(() => UUIDScalarType, { nullable: true })
@IsOptional()
@IsUUID('4')
unsubscribeTopicId?: string;
@Field(() => Int, { defaultValue: 30 })
@Min(1)
@Max(100)
limit: number;
@Field(() => Int, { defaultValue: 0 })
@Min(0)
offset: number;
}
@@ -0,0 +1,43 @@
import { Field, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { MessageSuppressionReason } from 'src/engine/core-modules/emailing-domain/types/message-suppression-reason.type';
import { MessageSuppressionSource } from 'src/engine/core-modules/emailing-domain/types/message-suppression-source.type';
registerEnumType(MessageSuppressionReason, {
name: 'MessageSuppressionReason',
});
registerEnumType(MessageSuppressionSource, {
name: 'MessageSuppressionSource',
});
@ObjectType('MessageSuppression')
export class MessageSuppressionDTO {
@Field(() => UUIDScalarType)
id: string;
@Field(() => Date)
createdAt: Date;
@Field(() => String)
emailAddress: string;
@Field(() => MessageSuppressionReason)
reason: MessageSuppressionReason;
@Field(() => MessageSuppressionSource)
source: MessageSuppressionSource;
@Field(() => UUIDScalarType, { nullable: true })
unsubscribeTopicId: string | null;
}
@ObjectType('MessageSuppressionList')
export class MessageSuppressionListDTO {
@Field(() => [MessageSuppressionDTO])
records: MessageSuppressionDTO[];
@Field(() => Int)
totalCount: number;
}
@@ -1,5 +1,11 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsEmail, IsNotEmpty, IsString, MaxLength } from 'class-validator';
import {
IsEmail,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
} from 'class-validator';
@InputType('CreateEmailGroupChannelInput')
export class CreateEmailGroupChannelInput {
@@ -9,4 +15,10 @@ export class CreateEmailGroupChannelInput {
@IsEmail()
@MaxLength(254)
handle: string;
@Field({ nullable: true })
@IsOptional()
@IsString()
@MaxLength(255)
displayName?: string;
}
@@ -39,6 +39,11 @@ export class MessageChannelDTO {
@Field()
handle: string;
@IsString()
@IsOptional()
@Field(() => String, { nullable: true })
displayName: string | null;
@IsEnum(MessageChannelType)
@IsNotEmpty()
@Field(() => MessageChannelType)
@@ -0,0 +1,16 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsOptional, IsString, MaxLength } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType('UpdateEmailGroupChannelInput')
export class UpdateEmailGroupChannelInput {
@Field(() => UUIDScalarType)
id: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
@MaxLength(255)
displayName?: string | null;
}
@@ -68,6 +68,9 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity {
@Column({ type: 'varchar', nullable: false })
handle: string;
@Column({ type: 'varchar', nullable: true })
displayName: string | null;
@Column({
type: 'enum',
enum: MessageChannelType,
@@ -213,10 +213,12 @@ export class MessageChannelMetadataService {
async createEmailGroupChannel({
handle,
displayName,
userWorkspaceId,
workspaceId,
}: {
handle: string;
displayName?: string;
userWorkspaceId: string;
workspaceId: string;
}): Promise<CreateEmailGroupChannelOutput> {
@@ -268,9 +270,14 @@ export class MessageChannelMetadataService {
visibility: 'workspace',
});
const trimmedDisplayName = displayName?.trim();
const messageChannel = await this.create({
workspaceId,
handle: forwardingAddress,
displayName: isNonEmptyString(trimmedDisplayName)
? trimmedDisplayName
: null,
connectedAccountId: connectedAccount.id,
type: MessageChannelType.EMAIL_GROUP,
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
@@ -340,6 +347,48 @@ export class MessageChannelMetadataService {
return messageChannel;
}
async updateEmailGroupChannel({
id,
displayName,
userWorkspaceId,
workspaceId,
}: {
id: string;
displayName?: string | null;
userWorkspaceId: string;
workspaceId: string;
}): Promise<MessageChannelDTO> {
const messageChannel = await this.verifyOwnership({
id,
userWorkspaceId,
workspaceId,
});
if (messageChannel.type !== MessageChannelType.EMAIL_GROUP) {
throw new MessageChannelException(
`Message channel ${id} is not an email group`,
MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT,
);
}
// An omitted displayName leaves the current one untouched; an explicit null clears it
if (displayName === undefined) {
return messageChannel;
}
const trimmedDisplayName = displayName?.trim();
return this.update({
id,
workspaceId,
data: {
displayName: isNonEmptyString(trimmedDisplayName)
? trimmedDisplayName
: null,
},
});
}
async deleteEmailGroupChannel({
id,
userWorkspaceId,
@@ -17,6 +17,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
import { ConnectedAccountPublicDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account-public.dto';
import { CreateEmailGroupChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/create-email-group-channel.input';
import { UpdateEmailGroupChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/update-email-group-channel.input';
import { CreateEmailGroupChannelOutput } from 'src/engine/metadata-modules/message-channel/dtos/create-email-group-channel.output';
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
import { UpdateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/update-message-channel.input';
@@ -174,6 +175,22 @@ export class MessageChannelResolver {
): Promise<CreateEmailGroupChannelOutput> {
return this.messageChannelMetadataService.createEmailGroupChannel({
handle: input.handle,
displayName: input.displayName,
userWorkspaceId,
workspaceId: workspace.id,
});
}
@Mutation(() => MessageChannelDTO)
@UseGuards(NoPermissionGuard)
async updateEmailGroupChannel(
@Args('input') input: UpdateEmailGroupChannelInput,
@AuthWorkspace() workspace: WorkspaceEntity,
@AuthUserWorkspaceId() userWorkspaceId: string,
): Promise<MessageChannelDTO> {
return this.messageChannelMetadataService.updateEmailGroupChannel({
id: input.id,
displayName: input.displayName,
userWorkspaceId,
workspaceId: workspace.id,
});
@@ -16,6 +16,7 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { UnsubscribeController } from 'src/modules/emailing/controllers/unsubscribe.controller';
import { EmailingSendResolver } from 'src/modules/emailing/resolvers/emailing-send.resolver';
import { MessageSuppressionResolver } from 'src/modules/emailing/resolvers/message-suppression.resolver';
import { UnsubscribeTopicResolver } from 'src/modules/emailing/resolvers/unsubscribe-topic.resolver';
import { EmailBillingService } from 'src/modules/emailing/services/email-billing.service';
import { EmailingDomainSenderService } from 'src/modules/emailing/services/emailing-domain-sender.service';
@@ -50,6 +51,7 @@ import { UnsubscribeTopicService } from 'src/modules/emailing/services/unsubscri
UnsubscribeTopicService,
EmailingDomainSenderService,
EmailingSendResolver,
MessageSuppressionResolver,
UnsubscribeTopicResolver,
provideWorkspaceScopedRepository(EmailingDomainEntity),
provideWorkspaceScopedRepository(MessageSuppressionEntity),
@@ -0,0 +1,58 @@
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { Args, Query } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { FeatureFlagKey } from 'twenty-shared/types';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { FindMessageSuppressionsInput } from 'src/engine/core-modules/emailing-domain/dtos/find-message-suppressions.input';
import { MessageSuppressionListDTO } from 'src/engine/core-modules/emailing-domain/dtos/message-suppression.dto';
import { EmailGroupAccessGraphqlApiExceptionFilter } from 'src/engine/core-modules/emailing-domain/filters/email-group-access-graphql-api-exception.filter';
import { EmailingDomainGraphqlApiExceptionFilter } from 'src/engine/core-modules/emailing-domain/filters/emailing-domain-graphql-api-exception.filter';
import { EmailGroupAccessService } from 'src/engine/core-modules/emailing-domain/services/email-group-access.service';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import {
FeatureFlagGuard,
RequireFeatureFlag,
} from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { MessageSuppressionService } from 'src/modules/emailing/services/message-suppression.service';
@UseGuards(
WorkspaceAuthGuard,
FeatureFlagGuard,
SettingsPermissionGuard(PermissionFlagType.WORKSPACE),
)
@UseFilters(
EmailGroupAccessGraphqlApiExceptionFilter,
EmailingDomainGraphqlApiExceptionFilter,
)
@UsePipes(ResolverValidationPipe)
@MetadataResolver(() => MessageSuppressionListDTO)
export class MessageSuppressionResolver {
constructor(
private readonly messageSuppressionService: MessageSuppressionService,
private readonly emailGroupAccessService: EmailGroupAccessService,
) {}
@Query(() => MessageSuppressionListDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_EMAIL_GROUP_ENABLED)
async messageSuppressions(
@Args('input') input: FindMessageSuppressionsInput,
@AuthWorkspace() currentWorkspace: WorkspaceEntity,
): Promise<MessageSuppressionListDTO> {
this.emailGroupAccessService.validateEmailGroupAccessOrThrow();
return this.messageSuppressionService.findSuppressions({
workspaceId: currentWorkspace.id,
reason: input.reason,
searchTerm: input.searchTerm,
unsubscribeTopicId: input.unsubscribeTopicId,
limit: input.limit,
offset: input.offset,
});
}
}
@@ -10,9 +10,7 @@ import { UnsubscribeTopicDTO } from 'src/engine/core-modules/emailing-domain/dto
import { UpdateUnsubscribeTopicInput } from 'src/engine/core-modules/emailing-domain/dtos/update-unsubscribe-topic.input';
import { EmailGroupAccessGraphqlApiExceptionFilter } from 'src/engine/core-modules/emailing-domain/filters/email-group-access-graphql-api-exception.filter';
import { EmailGroupAccessService } from 'src/engine/core-modules/emailing-domain/services/email-group-access.service';
import { UnsubscribeTokenService } from 'src/engine/core-modules/emailing-domain/services/unsubscribe-token.service';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import {
@@ -23,8 +21,6 @@ import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.g
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { UnsubscribeTopicService } from 'src/modules/emailing/services/unsubscribe-topic.service';
const UNSUBSCRIBE_PREVIEW_PLACEHOLDER_EMAIL = 'preview@example.com';
@UseGuards(
WorkspaceAuthGuard,
FeatureFlagGuard,
@@ -36,8 +32,6 @@ const UNSUBSCRIBE_PREVIEW_PLACEHOLDER_EMAIL = 'preview@example.com';
export class UnsubscribeTopicResolver {
constructor(
private readonly unsubscribeTopicService: UnsubscribeTopicService,
private readonly unsubscribeTokenService: UnsubscribeTokenService,
private readonly twentyConfigService: TwentyConfigService,
private readonly emailGroupAccessService: EmailGroupAccessService,
) {}
@@ -53,22 +47,6 @@ export class UnsubscribeTopicResolver {
);
}
@Query(() => String)
@RequireFeatureFlag(FeatureFlagKey.IS_EMAIL_GROUP_ENABLED)
unsubscribePagePreviewUrl(
@AuthWorkspace() currentWorkspace: WorkspaceEntity,
): string {
this.emailGroupAccessService.validateEmailGroupAccessOrThrow();
const token = this.unsubscribeTokenService.sign({
workspaceId: currentWorkspace.id,
emailAddress: UNSUBSCRIBE_PREVIEW_PLACEHOLDER_EMAIL,
preview: true,
});
return `${this.twentyConfigService.get('SERVER_URL')}/emailing/unsubscribe?t=${token}`;
}
@Mutation(() => UnsubscribeTopicDTO)
@RequireFeatureFlag(FeatureFlagKey.IS_EMAIL_GROUP_ENABLED)
async createUnsubscribeTopic(
@@ -16,6 +16,7 @@ import { type EmailingDomainEmailContent } from 'src/engine/core-modules/emailin
import { type EmailingDomainSendEmailRequest } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-input.type';
import { type EmailingDomainSendEmailResult } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain-send-email-result.type';
import { EmailingDomainEntity } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
import { formatMessageFromHeader } from 'src/modules/messaging/message-outbound-manager/utils/format-message-from-header.util';
import { MessageSuppressionService } from 'src/modules/emailing/services/message-suppression.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
import { type DeliverableRecipients } from 'src/engine/core-modules/emailing-domain/types/deliverable-recipients.type';
@@ -53,10 +54,19 @@ export class EmailingDomainSenderService {
emailContent,
);
const replyTo = await this.resolveReplyTo(workspaceId, emailContent);
const emailGroupChannel = await this.findEmailGroupChannel(
workspaceId,
emailContent.from,
);
const replyTo = this.resolveReplyTo(emailContent, emailGroupChannel);
const emailToSend: EmailingDomainSendEmailRequest = {
...emailContent,
from: formatMessageFromHeader({
fromEmail: emailContent.from,
fromName: emailGroupChannel?.displayName,
}),
workspaceId,
domain: emailingDomain.domain,
emailingDomain,
@@ -71,22 +81,27 @@ export class EmailingDomainSenderService {
.sendEmail(emailToSend);
}
private async resolveReplyTo(
private async findEmailGroupChannel(
workspaceId: string,
emailContent: EmailingDomainEmailContent,
): Promise<string[] | undefined> {
if (isDefined(emailContent.replyTo) && emailContent.replyTo.length > 0) {
return emailContent.replyTo;
}
const emailGroupChannel = await this.messageChannelRepository.findOne({
fromAddress: string,
): Promise<MessageChannelEntity | null> {
return this.messageChannelRepository.findOne({
where: {
workspaceId,
type: MessageChannelType.EMAIL_GROUP,
connectedAccount: { handle: emailContent.from },
connectedAccount: { handle: fromAddress },
},
relations: { connectedAccount: true },
});
}
private resolveReplyTo(
emailContent: EmailingDomainEmailContent,
emailGroupChannel: MessageChannelEntity | null,
): string[] | undefined {
if (isDefined(emailContent.replyTo) && emailContent.replyTo.length > 0) {
return emailContent.replyTo;
}
const forwardingAddress = emailGroupChannel?.handle;
@@ -1,8 +1,12 @@
import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { In, IsNull, QueryFailedError } from 'typeorm';
import {
escapeForIlike,
isDefined,
isNonEmptyArray,
} from 'twenty-shared/utils';
import { ILike, In, IsNull, QueryFailedError } from 'typeorm';
import { POSTGRESQL_ERROR_CODES } from 'src/engine/api/graphql/workspace-query-runner/constants/postgres-error-codes.constants';
import { type QueryFailedErrorWithCode } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
@@ -18,6 +22,15 @@ import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
import { UnsubscribeTopicService } from 'src/modules/emailing/services/unsubscribe-topic.service';
type FindSuppressionsArgs = {
workspaceId: string;
reason?: MessageSuppressionReason;
searchTerm?: string;
unsubscribeTopicId?: string;
limit: number;
offset: number;
};
type SuppressArgs = {
workspaceId: string;
emailAddress: string;
@@ -46,6 +59,38 @@ export class MessageSuppressionService {
private readonly unsubscribeTopicService: UnsubscribeTopicService,
) {}
async findSuppressions({
workspaceId,
reason,
searchTerm,
unsubscribeTopicId,
limit,
offset,
}: FindSuppressionsArgs): Promise<{
records: MessageSuppressionEntity[];
totalCount: number;
}> {
const [records, totalCount] = await this.suppressionRepository.findAndCount(
workspaceId,
{
where: {
...(isDefined(reason) ? { reason } : {}),
...(isNonEmptyString(unsubscribeTopicId)
? { unsubscribeTopicId }
: {}),
...(isNonEmptyString(searchTerm)
? { emailAddress: ILike(`%${escapeForIlike(searchTerm)}%`) }
: {}),
},
order: { createdAt: 'DESC' },
take: limit,
skip: offset,
},
);
return { records, totalCount };
}
async getSuppressedAddresses(
workspaceId: string,
emailAddresses: string[],
@@ -33,6 +33,7 @@ export enum SettingsPath {
NewEmailGroupChannel = 'email/new-email-group',
NewUnsubscribeTopic = 'email/new-unsubscribe-topic',
UnsubscribeTopicDetail = 'email/unsubscribe-topic/:unsubscribeTopicId',
Unsubscribe = 'communications/unsubscribe',
PublicDomain = 'applications/public-domain',
NewApprovedAccessDomain = 'security/approved-access-domain/new',
Community = 'community',