From b3666abd7f5bf2f77fe59b8aeaeb47e7f9279ecc Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Mon, 27 May 2024 14:37:04 +0200 Subject: [PATCH 01/13] Add brevo config api entity and resolver --- packages/api/generate-schema.ts | 10 ++- packages/api/schema.gql | 22 ++++++ .../brevo-api/brevo-api-campaigns.service.ts | 30 +++++++- .../src/brevo-config/brevo-config.module.ts | 24 ++++++ .../src/brevo-config/brevo-config.resolver.ts | 77 +++++++++++++++++++ .../brevo-config/dto/brevo-config.input.ts | 20 +++++ .../entities/brevo-config-entity.factory.ts | 60 +++++++++++++++ packages/api/src/brevo-module.ts | 8 ++ .../api/src/config/brevo-module.config.ts | 4 - .../email-campaign/email-campaign.module.ts | 6 +- .../email-campaign/email-campaigns.service.ts | 18 ++++- 11 files changed, 266 insertions(+), 13 deletions(-) create mode 100644 packages/api/src/brevo-config/brevo-config.module.ts create mode 100644 packages/api/src/brevo-config/brevo-config.resolver.ts create mode 100644 packages/api/src/brevo-config/dto/brevo-config.input.ts create mode 100644 packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts diff --git a/packages/api/generate-schema.ts b/packages/api/generate-schema.ts index be784d76..52b269b9 100644 --- a/packages/api/generate-schema.ts +++ b/packages/api/generate-schema.ts @@ -6,6 +6,8 @@ import { Field, GraphQLSchemaBuilderModule, GraphQLSchemaFactory, InputType, Obj import { writeFile } from "fs/promises"; import { printSchema } from "graphql"; +import { createBrevoConfigResolver } from "./src/brevo-config/brevo-config.resolver"; +import { BrevoConfigEntityFactory } from "./src/brevo-config/entities/brevo-config-entity.factory"; import { createBrevoContactResolver } from "./src/brevo-contact/brevo-contact.resolver"; import { BrevoContactFactory } from "./src/brevo-contact/dto/brevo-contact.factory"; import { SubscribeInputFactory } from "./src/brevo-contact/dto/subscribe-input.factory"; @@ -76,7 +78,13 @@ async function generateSchema(): Promise { Scope: EmailCampaignScope, }); - const schema = await gqlSchemaFactory.create([BrevoContactResolver, TargetGroupResolver, EmailCampaignResolver]); + const BrevoConfig = BrevoConfigEntityFactory.create({ Scope: EmailCampaignScope }); + const BrevoConfigResolver = createBrevoConfigResolver({ + BrevoConfig, + Scope: EmailCampaignScope, + }); + + const schema = await gqlSchemaFactory.create([BrevoContactResolver, TargetGroupResolver, EmailCampaignResolver, BrevoConfigResolver]); await writeFile("schema.gql", printSchema(schema)); diff --git a/packages/api/schema.gql b/packages/api/schema.gql index 060eab10..9b77f635 100644 --- a/packages/api/schema.gql +++ b/packages/api/schema.gql @@ -159,6 +159,15 @@ type PaginatedEmailCampaigns { totalCount: Int! } +type BrevoConfig implements DocumentInterface { + id: ID! + updatedAt: DateTime! + senderMail: String! + senderName: String! + createdAt: DateTime! + scope: EmailCampaignContentScope! +} + input EmailCampaignContentScopeInput { thisScopeHasNoFields____: String } @@ -171,6 +180,7 @@ type Query { emailCampaign(id: ID!): EmailCampaign! emailCampaigns(scope: EmailCampaignContentScopeInput!, search: String, filter: EmailCampaignFilter, sort: [EmailCampaignSort!], offset: Int! = 0, limit: Int! = 25): PaginatedEmailCampaigns! emailCampaignStatistics(id: ID!): BrevoApiCampaignStatistics + brevoConfig(scope: EmailCampaignContentScopeInput!): BrevoConfig } input TargetGroupFilter { @@ -249,6 +259,8 @@ type Mutation { deleteEmailCampaign(id: ID!): Boolean! sendEmailCampaignNow(id: ID!): Boolean! sendEmailCampaignToTestEmails(id: ID!, data: SendTestEmailCampaignArgs!): Boolean! + createBrevoConfig(scope: EmailCampaignContentScopeInput!, input: BrevoConfigInput!): BrevoConfig! + updateBrevoConfig(id: ID!, input: BrevoConfigUpdateInput!, lastUpdatedAt: DateTime): BrevoConfig! } input BrevoContactUpdateInput { @@ -302,3 +314,13 @@ input EmailCampaignUpdateInput { input SendTestEmailCampaignArgs { emails: [String!]! } + +input BrevoConfigInput { + senderMail: String! + senderName: String! +} + +input BrevoConfigUpdateInput { + senderMail: String + senderName: String +} diff --git a/packages/api/src/brevo-api/brevo-api-campaigns.service.ts b/packages/api/src/brevo-api/brevo-api-campaigns.service.ts index b1bd251a..5158b452 100644 --- a/packages/api/src/brevo-api/brevo-api-campaigns.service.ts +++ b/packages/api/src/brevo-api/brevo-api-campaigns.service.ts @@ -27,13 +27,23 @@ export class BrevoApiCampaignsService { return SendingState.DRAFT; } - public async createBrevoCampaign(campaign: EmailCampaignInterface, htmlContent: string, scheduledAt?: Date): Promise { + public async createBrevoCampaign({ + campaign, + htmlContent, + sender, + scheduledAt, + }: { + campaign: EmailCampaignInterface; + htmlContent: string; + sender: { name: string; mail: string }; + scheduledAt?: Date; + }): Promise { const targetGroup = await campaign.targetGroup?.load(); const emailCampaign = { name: campaign.title, subject: campaign.subject, - sender: { name: this.config.brevo.sender.name, email: this.config.brevo.sender.email }, + sender: { name: sender.name, email: sender.mail }, recipients: { listIds: targetGroup ? [targetGroup?.brevoId] : [] }, htmlContent, scheduledAt: scheduledAt?.toISOString(), @@ -43,13 +53,25 @@ export class BrevoApiCampaignsService { return data.body.id; } - public async updateBrevoCampaign(id: number, campaign: EmailCampaignInterface, htmlContent: string, scheduledAt?: Date): Promise { + public async updateBrevoCampaign({ + id, + campaign, + htmlContent, + scheduledAt, + sender, + }: { + id: number; + campaign: EmailCampaignInterface; + htmlContent: string; + sender: { name: string; mail: string }; + scheduledAt?: Date; + }): Promise { const targetGroup = await campaign.targetGroup?.load(); const emailCampaign = { name: campaign.title, subject: campaign.subject, - sender: { name: this.config.brevo.sender.name, email: this.config.brevo.sender.email }, + sender: { name: sender.name, mail: sender.mail }, recipients: { listIds: targetGroup ? [targetGroup?.brevoId] : [] }, htmlContent, scheduledAt: scheduledAt?.toISOString(), diff --git a/packages/api/src/brevo-config/brevo-config.module.ts b/packages/api/src/brevo-config/brevo-config.module.ts new file mode 100644 index 00000000..3ceab913 --- /dev/null +++ b/packages/api/src/brevo-config/brevo-config.module.ts @@ -0,0 +1,24 @@ +import { MikroOrmModule } from "@mikro-orm/nestjs"; +import { DynamicModule, Module, Type } from "@nestjs/common"; + +import { EmailCampaignScopeInterface } from "../types"; +import { createBrevoConfigResolver } from "./brevo-config.resolver"; +import { BrevoConfigInterface } from "./entities/brevo-config-entity.factory"; + +interface BrevoConfigModuleConfig { + Scope: Type; + BrevoConfig: Type; +} + +@Module({}) +export class BrevoConfigModule { + static register({ Scope, BrevoConfig }: BrevoConfigModuleConfig): DynamicModule { + const BrevoConfigResolver = createBrevoConfigResolver({ BrevoConfig, Scope }); + + return { + module: BrevoConfigModule, + imports: [MikroOrmModule.forFeature([BrevoConfig])], + providers: [BrevoConfigResolver], + }; + } +} diff --git a/packages/api/src/brevo-config/brevo-config.resolver.ts b/packages/api/src/brevo-config/brevo-config.resolver.ts new file mode 100644 index 00000000..e9f985d0 --- /dev/null +++ b/packages/api/src/brevo-config/brevo-config.resolver.ts @@ -0,0 +1,77 @@ +import { AffectedEntity, RequiredPermission, validateNotModified } from "@comet/cms-api"; +import { EntityManager, EntityRepository, wrap } from "@mikro-orm/core"; +import { InjectRepository } from "@mikro-orm/nestjs"; +import { Type } from "@nestjs/common"; +import { Args, ID, Mutation, Query, Resolver } from "@nestjs/graphql"; + +import { EmailCampaignScopeInterface } from "../types"; +import { DynamicDtoValidationPipe } from "../validation/dynamic-dto-validation.pipe"; +import { BrevoConfigInput, BrevoConfigUpdateInput } from "./dto/brevo-config.input"; +import { BrevoConfigInterface } from "./entities/brevo-config-entity.factory"; + +export function createBrevoConfigResolver({ + Scope, + BrevoConfig, +}: { + Scope: Type; + BrevoConfig: Type; +}): Type { + @Resolver(() => BrevoConfig) + @RequiredPermission(["brevo-newsletter"]) + class BrevoConfigResolver { + constructor( + private readonly entityManager: EntityManager, + @InjectRepository(BrevoConfig) private readonly repository: EntityRepository, + ) {} + + @Query(() => BrevoConfig, { nullable: true }) + async brevoConfig( + @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) + scope: typeof Scope, + ): Promise { + const brevoConfig = await this.repository.findOne({ scope }); + return brevoConfig; + } + + // TODO: add validation if the input contains a valid sender + + @Mutation(() => BrevoConfig) + async createBrevoConfig( + @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) + scope: typeof Scope, + @Args("input", { type: () => BrevoConfigInput }) input: BrevoConfigInput, + ): Promise { + const brevoConfig = this.repository.create({ + ...input, + scope, + }); + + await this.entityManager.flush(); + + return brevoConfig; + } + + @Mutation(() => BrevoConfig) + @AffectedEntity(BrevoConfig) + async updateBrevoConfig( + @Args("id", { type: () => ID }) id: string, + @Args("input", { type: () => BrevoConfigUpdateInput }) input: BrevoConfigUpdateInput, + @Args("lastUpdatedAt", { type: () => Date, nullable: true }) lastUpdatedAt?: Date, + ): Promise { + const brevoConfig = await this.repository.findOneOrFail(id); + if (lastUpdatedAt) { + validateNotModified(brevoConfig, lastUpdatedAt); + } + + wrap(brevoConfig).assign({ + ...input, + }); + + await this.entityManager.flush(); + + return brevoConfig; + } + } + + return BrevoConfigResolver; +} diff --git a/packages/api/src/brevo-config/dto/brevo-config.input.ts b/packages/api/src/brevo-config/dto/brevo-config.input.ts new file mode 100644 index 00000000..1ceffdc5 --- /dev/null +++ b/packages/api/src/brevo-config/dto/brevo-config.input.ts @@ -0,0 +1,20 @@ +import { PartialType } from "@comet/cms-api"; +import { Field, InputType } from "@nestjs/graphql"; +import { IsEmail, IsNotEmpty, IsString } from "class-validator"; + +@InputType() +export class BrevoConfigInput { + @IsNotEmpty() + @IsString() + @Field() + @IsEmail() + senderMail: string; + + @IsNotEmpty() + @IsString() + @Field() + senderName: string; +} + +@InputType() +export class BrevoConfigUpdateInput extends PartialType(BrevoConfigInput) {} diff --git a/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts b/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts new file mode 100644 index 00000000..57bd5fdd --- /dev/null +++ b/packages/api/src/brevo-config/entities/brevo-config-entity.factory.ts @@ -0,0 +1,60 @@ +import { DocumentInterface } from "@comet/cms-api"; +import { Embedded, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"; +import { Type } from "@nestjs/common"; +import { Field, ID, ObjectType } from "@nestjs/graphql"; +import { v4 } from "uuid"; + +import { EmailCampaignScopeInterface } from "../../types"; + +export interface BrevoConfigInterface { + [OptionalProps]?: "createdAt" | "updatedAt"; + id: string; + senderName: string; + senderMail: string; + createdAt: Date; + updatedAt: Date; + scope: EmailCampaignScopeInterface; +} + +export class BrevoConfigEntityFactory { + static create({ Scope }: { Scope: EmailCampaignScopeInterface }): Type { + @Entity() + @ObjectType({ + implements: () => [DocumentInterface], + }) + class BrevoConfig implements BrevoConfigInterface, DocumentInterface { + [OptionalProps]?: "createdAt" | "updatedAt"; + + @PrimaryKey({ columnType: "uuid" }) + @Field(() => ID) + id: string = v4(); + + @Property({ columnType: "text" }) + @Field() + senderMail: string; + + @Property({ columnType: "text" }) + @Field() + senderName: string; + + @Property({ + columnType: "timestamp with time zone", + }) + @Field() + createdAt: Date = new Date(); + + @Property({ + columnType: "timestamp with time zone", + onUpdate: () => new Date(), + }) + @Field() + updatedAt: Date = new Date(); + + @Embedded(() => Scope) + @Field(() => Scope) + scope: typeof Scope; + } + + return BrevoConfig; + } +} diff --git a/packages/api/src/brevo-module.ts b/packages/api/src/brevo-module.ts index 36cab741..c96ed65a 100644 --- a/packages/api/src/brevo-module.ts +++ b/packages/api/src/brevo-module.ts @@ -1,6 +1,8 @@ import { DynamicModule, Global, Module } from "@nestjs/common"; import { BrevoApiModule } from "./brevo-api/brevo-api.module"; +import { BrevoConfigModule } from "./brevo-config/brevo-config.module"; +import { BrevoConfigEntityFactory } from "./brevo-config/entities/brevo-config-entity.factory"; import { BrevoContactModule } from "./brevo-contact/brevo-contact.module"; import { BrevoModuleConfig } from "./config/brevo-module.config"; import { ConfigModule } from "./config/config.module"; @@ -17,6 +19,10 @@ export class BrevoModule { BrevoFilterAttributes: config.brevo.BrevoContactFilterAttributes, }); + const BrevoConfig = BrevoConfigEntityFactory.create({ + Scope: config.emailCampaigns.Scope, + }); + return { module: BrevoModule, imports: [ @@ -30,12 +36,14 @@ export class BrevoModule { EmailCampaignContentBlock: config.emailCampaigns.EmailCampaignContentBlock, Scope: config.emailCampaigns.Scope, TargetGroup, + BrevoConfig, }), TargetGroupModule.register({ Scope: config.emailCampaigns.Scope, BrevoFilterAttributes: config.brevo.BrevoContactFilterAttributes, TargetGroup: TargetGroup, }), + BrevoConfigModule.register({ BrevoConfig, Scope: config.emailCampaigns.Scope }), ConfigModule.forRoot(config), ], exports: [TargetGroupModule], diff --git a/packages/api/src/config/brevo-module.config.ts b/packages/api/src/config/brevo-module.config.ts index fc5829ec..7202b16c 100644 --- a/packages/api/src/config/brevo-module.config.ts +++ b/packages/api/src/config/brevo-module.config.ts @@ -11,10 +11,6 @@ export interface BrevoModuleConfig { BrevoContactFilterAttributes?: Type; doubleOptInTemplateId: number; allowedRedirectUrl: string; - sender: { - name: string; - email: string; - }; }; ecgRtrList: { apiKey: string; diff --git a/packages/api/src/email-campaign/email-campaign.module.ts b/packages/api/src/email-campaign/email-campaign.module.ts index 4d1ed54b..b130633d 100644 --- a/packages/api/src/email-campaign/email-campaign.module.ts +++ b/packages/api/src/email-campaign/email-campaign.module.ts @@ -2,6 +2,7 @@ import { Block } from "@comet/blocks-api"; import { MikroOrmModule } from "@mikro-orm/nestjs"; import { HttpModule } from "@nestjs/axios"; import { DynamicModule, Module, Type } from "@nestjs/common"; +import { BrevoConfigInterface } from "src/brevo-config/entities/brevo-config-entity.factory"; import { TargetGroupInterface } from "src/target-group/entity/target-group-entity.factory"; import { BrevoApiModule } from "../brevo-api/brevo-api.module"; @@ -17,11 +18,12 @@ interface EmailCampaignModuleConfig { Scope: Type; EmailCampaignContentBlock: Block; TargetGroup: Type; + BrevoConfig: Type; } @Module({}) export class EmailCampaignModule { - static register({ Scope, EmailCampaignContentBlock, TargetGroup }: EmailCampaignModuleConfig): DynamicModule { + static register({ Scope, EmailCampaignContentBlock, TargetGroup, BrevoConfig }: EmailCampaignModuleConfig): DynamicModule { const EmailCampaign = EmailCampaignEntityFactory.create({ Scope, EmailCampaignContentBlock, @@ -44,7 +46,7 @@ export class EmailCampaignModule { HttpModule.register({ timeout: 5000, }), - MikroOrmModule.forFeature([EmailCampaign, TargetGroup]), + MikroOrmModule.forFeature([EmailCampaign, TargetGroup, BrevoConfig]), ], providers: [EmailCampaignsResolver, EmailCampaignsService, EcgRtrListService], }; diff --git a/packages/api/src/email-campaign/email-campaigns.service.ts b/packages/api/src/email-campaign/email-campaigns.service.ts index 7c6f9854..f5c66a78 100644 --- a/packages/api/src/email-campaign/email-campaigns.service.ts +++ b/packages/api/src/email-campaign/email-campaigns.service.ts @@ -7,6 +7,7 @@ import { UpdateCampaignStatus } from "@sendinblue/client"; import { BrevoApiCampaignsService } from "../brevo-api/brevo-api-campaigns.service"; import { BrevoApiContactsService } from "../brevo-api/brevo-api-contact.service"; +import { BrevoConfigInterface } from "../brevo-config/entities/brevo-config-entity.factory"; import { EcgRtrListService } from "../brevo-contact/ecg-rtr-list/ecg-rtr-list.service"; import { BrevoModuleConfig } from "../config/brevo-module.config"; import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants"; @@ -18,6 +19,7 @@ export class EmailCampaignsService { constructor( @Inject(BREVO_MODULE_CONFIG) private readonly config: BrevoModuleConfig, @InjectRepository("EmailCampaign") private readonly repository: EntityRepository, + @InjectRepository("BrevoConfig") private readonly brevoConfigRepository: EntityRepository, private readonly httpService: HttpService, private readonly brevoApiCampaignService: BrevoApiCampaignsService, private readonly brevoApiContactsService: BrevoApiContactsService, @@ -42,6 +44,7 @@ export class EmailCampaignsService { async saveEmailCampaignInBrevo(id: string, scheduledAt?: Date): Promise { const campaign = await this.repository.findOneOrFail(id); + const brevoConfig = await this.brevoConfigRepository.findOneOrFail({ scope: campaign.scope }); const content = await this.blockTransformerService.transformToPlain(campaign.content); @@ -63,13 +66,24 @@ export class EmailCampaignsService { let brevoId = campaign.brevoId; if (!brevoId) { - brevoId = await this.brevoApiCampaignService.createBrevoCampaign(campaign, htmlContent, scheduledAt); + brevoId = await this.brevoApiCampaignService.createBrevoCampaign({ + campaign, + htmlContent, + scheduledAt, + sender: { name: brevoConfig.senderName, mail: brevoConfig.senderMail }, + }); wrap(campaign).assign({ brevoId }); await this.entityManager.flush(); } else { - await this.brevoApiCampaignService.updateBrevoCampaign(brevoId, campaign, htmlContent, scheduledAt); + await this.brevoApiCampaignService.updateBrevoCampaign({ + id: brevoId, + campaign, + htmlContent, + scheduledAt, + sender: { name: brevoConfig.senderName, mail: brevoConfig.senderMail }, + }); } return campaign; From 2ebd47f392414947b2ac92941da2cfd9138dcba4 Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Mon, 27 May 2024 14:47:15 +0200 Subject: [PATCH 02/13] Add brevo config admin page --- .../brevoConfiguration/BrevoConfigForm.gql.ts | 49 ++++++ .../brevoConfiguration/BrevoConfigForm.tsx | 142 ++++++++++++++++++ .../brevoConfiguration/BrevoConfigPage.tsx | 22 +++ packages/admin/src/index.ts | 1 + 4 files changed, 214 insertions(+) create mode 100644 packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts create mode 100644 packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx create mode 100644 packages/admin/src/brevoConfiguration/BrevoConfigPage.tsx diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts b/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts new file mode 100644 index 00000000..2f048916 --- /dev/null +++ b/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts @@ -0,0 +1,49 @@ +import { gql } from "@apollo/client"; + +export const brevoConfigFormFragment = gql` + fragment BrevoConfigForm on BrevoConfig { + senderMail + senderName + } +`; + +export const brevoConfigFormQuery = gql` + query BrevoConfigForm($scope: EmailCampaignContentScopeInput!) { + brevoConfig(scope: $scope) { + id + updatedAt + ...BrevoConfigForm + } + } + ${brevoConfigFormFragment} +`; + +export const brevoConfigFormCheckForChangesQuery = gql` + query BrevoConfigFormCheckForChanges($scope: EmailCampaignContentScopeInput!) { + brevoConfig(scope: $scope) { + updatedAt + } + } +`; + +export const createBrevoConfigMutation = gql` + mutation CreateBrevoConfig($scope: EmailCampaignContentScopeInput!, $input: BrevoConfigInput!) { + createBrevoConfig(scope: $scope, input: $input) { + id + updatedAt + ...BrevoConfigForm + } + } + ${brevoConfigFormFragment} +`; + +export const updateBrevoConfigMutation = gql` + mutation UpdateBrevoConfig($id: ID!, $input: BrevoConfigUpdateInput!, $lastUpdatedAt: DateTime) { + updateBrevoConfig(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt) { + id + updatedAt + ...BrevoConfigForm + } + } + ${brevoConfigFormFragment} +`; diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx new file mode 100644 index 00000000..a31e82aa --- /dev/null +++ b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx @@ -0,0 +1,142 @@ +import { useApolloClient, useQuery } from "@apollo/client"; +import { + FinalForm, + FinalFormSaveSplitButton, + FinalFormSubmitEvent, + Loading, + MainContent, + TextField, + Toolbar, + ToolbarActions, + ToolbarFillSpace, + ToolbarTitleItem, + useFormApiRef, + useStackSwitchApi, +} from "@comet/admin"; +import { ContentScopeInterface, EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; +import { FormApi } from "final-form"; +import React from "react"; +import { FormattedMessage } from "react-intl"; + +import { brevoConfigFormQuery, createBrevoConfigMutation, updateBrevoConfigMutation } from "./BrevoConfigForm.gql"; +import { + GQLBrevoConfigFormFragment, + GQLBrevoConfigFormQuery, + GQLBrevoConfigFormQueryVariables, + GQLCreateBrevoConfigMutation, + GQLCreateBrevoConfigMutationVariables, + GQLUpdateBrevoConfigMutation, + GQLUpdateBrevoConfigMutationVariables, +} from "./BrevoConfigForm.gql.generated"; + +type FormValues = GQLBrevoConfigFormFragment; + +interface FormProps { + scope: ContentScopeInterface; +} + +export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { + const client = useApolloClient(); + const formApiRef = useFormApiRef(); + const stackSwitchApi = useStackSwitchApi(); + + const { data, error, loading, refetch } = useQuery(brevoConfigFormQuery, { + variables: { scope }, + }); + + const mode = data?.brevoConfig?.id ? "edit" : "add"; + + const initialValues = React.useMemo>( + () => + data?.brevoConfig + ? { + ...data.brevoConfig, + } + : {}, + [data], + ); + + const saveConflict = useFormSaveConflict({ + checkConflict: async () => { + const updatedAt = await queryUpdatedAt(client, "brevoConfig", data?.brevoConfig?.id); + return resolveHasSaveConflict(data?.brevoConfig?.updatedAt, updatedAt); + }, + formApiRef, + loadLatestVersion: async () => { + await refetch(); + }, + }); + + const handleSubmit = async (state: FormValues, form: FormApi, event: FinalFormSubmitEvent) => { + if (await saveConflict.checkForConflicts()) { + throw new Error("Conflicts detected"); + } + + const output = { + senderName: state.senderName.trim(), + senderMail: state.senderMail.trim(), + }; + + if (mode === "edit") { + if (!data?.brevoConfig?.id) { + throw new Error("Missing id in edit mode"); + } + await client.mutate({ + mutation: updateBrevoConfigMutation, + variables: { id: data?.brevoConfig?.id, input: output, lastUpdatedAt: data?.brevoConfig?.updatedAt }, + }); + } else { + const { data: mutationResponse } = await client.mutate({ + mutation: createBrevoConfigMutation, + variables: { scope, input: output }, + }); + if (!event.navigatingBack) { + const id = mutationResponse?.createBrevoConfig.id; + if (id) { + setTimeout(() => { + stackSwitchApi.activatePage("edit", id); + }); + } + } + } + }; + + if (error) throw error; + + if (loading) { + return ; + } + + return ( + apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}> + {({ values }) => ( + + {saveConflict.dialogs} + + + + + + + + + + + } + /> + } + /> + + + )} + + ); +} diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigPage.tsx b/packages/admin/src/brevoConfiguration/BrevoConfigPage.tsx new file mode 100644 index 00000000..f8c8163b --- /dev/null +++ b/packages/admin/src/brevoConfiguration/BrevoConfigPage.tsx @@ -0,0 +1,22 @@ +import { useContentScope } from "@comet/cms-admin"; +import * as React from "react"; + +import { BrevoConfigForm } from "./BrevoConfigForm"; + +interface CreateBrevoConfigPageOptions { + scopeParts: string[]; +} + +export function createBrevoConfigPage({ scopeParts }: CreateBrevoConfigPageOptions) { + function BrevoConfigPage(): JSX.Element { + const { scope: completeScope } = useContentScope(); + + const scope = scopeParts.reduce((acc, scopePart) => { + acc[scopePart] = completeScope[scopePart]; + return acc; + }, {} as { [key: string]: unknown }); + + return ; + } + return BrevoConfigPage; +} diff --git a/packages/admin/src/index.ts b/packages/admin/src/index.ts index 649ac487..33ed2f25 100644 --- a/packages/admin/src/index.ts +++ b/packages/admin/src/index.ts @@ -1,3 +1,4 @@ +export { createBrevoConfigPage } from "./brevoConfiguration/BrevoConfigPage"; export { createBrevoContactsPage } from "./brevoContacts/BrevoContactsPage"; export { createEmailCampaignsPage } from "./emailCampaigns/EmailCampaignsPage"; export { EditTargetGroupFinalFormValues } from "./targetGroups/TargetGroupForm"; From 209c8c9934c677d0288263c4149690549ba1505d Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Mon, 27 May 2024 14:49:18 +0200 Subject: [PATCH 03/13] Add brevo config to demo --- README.md | 7 ++++-- demo/admin/src/Routes.tsx | 7 +++++- demo/admin/src/common/MasterMenu.tsx | 4 ++++ demo/api/schema.gql | 22 +++++++++++++++++++ demo/api/src/app.module.ts | 4 ---- demo/api/src/config/config.ts | 4 ---- demo/api/src/config/environment-variables.ts | 8 +------ .../db/migrations/Migration20240527112204.ts | 9 ++++++++ 8 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 demo/api/src/db/migrations/Migration20240527112204.ts diff --git a/README.md b/README.md index 1cbf6858..816148e6 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,15 @@ The following variables must be set manually - `BREVO_API_KEY` - `BREVO_DOUBLE_OPT_IN_TEMPLATE_ID` -- `BREVO_SENDER_NAME` -- `BREVO_SENDER_EMAIL` - `ECG_RTR_LIST_API_KEY` - `CAMPAIGN_BASIC_AUTH_USERNAME` - `CAMPAIGN_BASIC_AUTH_PASSWORD` +### Configure brevo sender in the demo admin + +- Brevo sender name +- Brevo sender mail + ### Start development processes [dev-process-manager](https://github.com/vivid-planet/dev-process-manager) is used for local development. diff --git a/demo/admin/src/Routes.tsx b/demo/admin/src/Routes.tsx index bcb1db6b..d2e592eb 100644 --- a/demo/admin/src/Routes.tsx +++ b/demo/admin/src/Routes.tsx @@ -1,6 +1,6 @@ import { MasterLayout, RouteWithErrorBoundary } from "@comet/admin"; import { Domain } from "@comet/admin-icons"; -import { createBrevoContactsPage, createEmailCampaignsPage, createTargetGroupsPage } from "@comet/brevo-admin"; +import { createBrevoConfigPage, createBrevoContactsPage, createEmailCampaignsPage, createTargetGroupsPage } from "@comet/brevo-admin"; import { ContentScopeIndicator, createRedirectsPage, DamPage, PagesPage, PublisherPage, SitePreview } from "@comet/cms-admin"; import { getBrevoContactConfig } from "@src/common/brevoModuleConfig/brevoContactsPageAttributesConfig"; import { config } from "@src/config"; @@ -46,6 +46,10 @@ export const Routes: React.FC = () => { previewUrl: `${config.campaignUrl}/preview`, }); + const BrevoConfigPage = createBrevoConfigPage({ + scopeParts: ["domain", "language"], + }); + return ( {({ match }) => ( @@ -93,6 +97,7 @@ export const Routes: React.FC = () => { + diff --git a/demo/admin/src/common/MasterMenu.tsx b/demo/admin/src/common/MasterMenu.tsx index f71f8e97..1d175b63 100644 --- a/demo/admin/src/common/MasterMenu.tsx +++ b/demo/admin/src/common/MasterMenu.tsx @@ -48,6 +48,10 @@ export const MasterMenu: React.FC = () => { primary={intl.formatMessage({ id: "menu.newsletter.emailCampaigns", defaultMessage: "Email campaigns" })} to={`${match.url}/newsletter/email-campaigns`} /> + }> { + this.addSql( + 'create table "BrevoConfig" ("id" uuid not null, "createdAt" timestamp with time zone not null, "updatedAt" timestamp with time zone not null, "scope_domain" text not null, "scope_language" text not null, "senderMail" text not null, "senderName" text not null, constraint "BrevoConfig_pkey" primary key ("id"));', + ); + } +} From 2319ba2e4687ac65f3c4e1d57bc07106ed6e1d26 Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Mon, 27 May 2024 14:52:45 +0200 Subject: [PATCH 04/13] Add extra permission for brevo config resolver --- packages/api/src/brevo-config/brevo-config.resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/brevo-config/brevo-config.resolver.ts b/packages/api/src/brevo-config/brevo-config.resolver.ts index e9f985d0..ca2dff3a 100644 --- a/packages/api/src/brevo-config/brevo-config.resolver.ts +++ b/packages/api/src/brevo-config/brevo-config.resolver.ts @@ -17,7 +17,7 @@ export function createBrevoConfigResolver({ BrevoConfig: Type; }): Type { @Resolver(() => BrevoConfig) - @RequiredPermission(["brevo-newsletter"]) + @RequiredPermission(["brevo-newsletter-config"]) class BrevoConfigResolver { constructor( private readonly entityManager: EntityManager, From d748ce98e8fb00e591c95fa9ad9a55ec062ee08a Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Mon, 27 May 2024 16:14:20 +0200 Subject: [PATCH 05/13] Add available senders dropdown to brevo config --- demo/api/schema.gql | 8 ++ .../brevoConfiguration/BrevoConfigForm.gql.ts | 10 ++ .../brevoConfiguration/BrevoConfigForm.tsx | 125 +++++++++++------- packages/api/schema.gql | 8 ++ .../src/brevo-api/brevo-api-sender.service.ts | 26 ++++ .../api/src/brevo-api/brevo-api.module.ts | 5 +- .../api/src/brevo-api/dto/brevo-api-sender.ts | 18 +++ .../src/brevo-config/brevo-config.module.ts | 3 +- .../src/brevo-config/brevo-config.resolver.ts | 10 ++ 9 files changed, 162 insertions(+), 51 deletions(-) create mode 100644 packages/api/src/brevo-api/brevo-api-sender.service.ts create mode 100644 packages/api/src/brevo-api/dto/brevo-api-sender.ts diff --git a/demo/api/schema.gql b/demo/api/schema.gql index a17ec178..fccdb42a 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -163,6 +163,13 @@ type PaginatedUserList { totalCount: Int! } +type BrevoApiSender { + id: ID! + name: String! + email: String! + active: Boolean! +} + type BrevoApiCampaignStatistics { """Number of unique clicks for the campaign""" uniqueClicks: Int! @@ -548,6 +555,7 @@ type Query { emailCampaignStatistics(id: ID!): BrevoApiCampaignStatistics targetGroup(id: ID!): TargetGroup! targetGroups(scope: EmailCampaignContentScopeInput!, search: String, filter: TargetGroupFilter, sort: [TargetGroupSort!], offset: Int! = 0, limit: Int! = 25): PaginatedTargetGroups! + senders: [BrevoApiSender!] brevoConfig(scope: EmailCampaignContentScopeInput!): BrevoConfig } diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts b/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts index 2f048916..661f5a72 100644 --- a/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts +++ b/packages/admin/src/brevoConfiguration/BrevoConfigForm.gql.ts @@ -47,3 +47,13 @@ export const updateBrevoConfigMutation = gql` } ${brevoConfigFormFragment} `; + +export const sendersSelectQuery = gql` + query SendersSelect { + senders { + id + name + email + } + } +`; diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx index a31e82aa..9f95bb28 100644 --- a/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx +++ b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx @@ -1,11 +1,12 @@ import { useApolloClient, useQuery } from "@apollo/client"; import { + Field, FinalForm, + FinalFormAutocomplete, FinalFormSaveSplitButton, FinalFormSubmitEvent, Loading, MainContent, - TextField, Toolbar, ToolbarActions, ToolbarFillSpace, @@ -13,23 +14,30 @@ import { useFormApiRef, useStackSwitchApi, } from "@comet/admin"; -import { ContentScopeInterface, EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; +import { ContentScopeInterface, EditPageLayout, useFormSaveConflict } from "@comet/cms-admin"; import { FormApi } from "final-form"; import React from "react"; import { FormattedMessage } from "react-intl"; -import { brevoConfigFormQuery, createBrevoConfigMutation, updateBrevoConfigMutation } from "./BrevoConfigForm.gql"; +import { brevoConfigFormQuery, createBrevoConfigMutation, sendersSelectQuery, updateBrevoConfigMutation } from "./BrevoConfigForm.gql"; import { - GQLBrevoConfigFormFragment, GQLBrevoConfigFormQuery, GQLBrevoConfigFormQueryVariables, GQLCreateBrevoConfigMutation, GQLCreateBrevoConfigMutationVariables, + GQLSendersSelectQuery, + GQLSendersSelectQueryVariables, GQLUpdateBrevoConfigMutation, GQLUpdateBrevoConfigMutationVariables, } from "./BrevoConfigForm.gql.generated"; -type FormValues = GQLBrevoConfigFormFragment; +interface Option { + value: string; + label: string; +} +type FormValues = { + sender: Option; +}; interface FormProps { scope: ContentScopeInterface; @@ -44,22 +52,38 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { variables: { scope }, }); + const { + data: sendersData, + error: senderError, + loading: senderLoading, + } = useQuery(sendersSelectQuery); + + const senderOptions = + sendersData?.senders?.map((sender) => ({ + value: sender.email, + label: `${sender.name} (${sender.email})`, + })) ?? []; + const mode = data?.brevoConfig?.id ? "edit" : "add"; - const initialValues = React.useMemo>( - () => - data?.brevoConfig - ? { - ...data.brevoConfig, - } - : {}, - [data], - ); + const initialValues = React.useMemo>(() => { + const sender = sendersData?.senders?.find((s) => s.email === data?.brevoConfig?.senderMail && s.name === data?.brevoConfig?.senderName); + return sender + ? { + sender: { + value: sender.id, + label: `${sender.name} (${sender.email})`, + }, + } + : {}; + }, [data?.brevoConfig?.senderMail, data?.brevoConfig?.senderName, sendersData?.senders]); const saveConflict = useFormSaveConflict({ checkConflict: async () => { - const updatedAt = await queryUpdatedAt(client, "brevoConfig", data?.brevoConfig?.id); - return resolveHasSaveConflict(data?.brevoConfig?.updatedAt, updatedAt); + // TODO: + return false; + // const updatedAt = await queryUpdatedAt(client, "brevoConfig", scope); + // return resolveHasSaveConflict(data?.brevoConfig?.updatedAt, updatedAt); }, formApiRef, loadLatestVersion: async () => { @@ -72,9 +96,15 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { throw new Error("Conflicts detected"); } + const sender = sendersData?.senders?.find((s) => s.email === state.sender.value); + + if (!sender) { + throw new Error("No sender selected"); + } + const output = { - senderName: state.senderName.trim(), - senderMail: state.senderMail.trim(), + senderName: sender?.name, + senderMail: sender?.email, }; if (mode === "edit") { @@ -101,42 +131,41 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { } }; - if (error) throw error; + if (error || senderError) throw error ?? senderError; - if (loading) { + if (loading || senderLoading) { return ; } return ( apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}> - {({ values }) => ( - - {saveConflict.dialogs} - - - - - - - - - - - } - /> - } - /> - - - )} + {({ values }) => { + return ( + + {saveConflict.dialogs} + + + + + + + + + + + option.label} + isOptionEqualToValue={(option: Option, value: Option) => option.value === value.value} + options={senderOptions} + name="sender" + label={} + fullWidth + /> + + + ); + }} ); } diff --git a/packages/api/schema.gql b/packages/api/schema.gql index 9b77f635..4b515ae2 100644 --- a/packages/api/schema.gql +++ b/packages/api/schema.gql @@ -44,6 +44,13 @@ type User { language: String! } +type BrevoApiSender { + id: ID! + name: String! + email: String! + active: Boolean! +} + type BrevoApiCampaignStatistics { """Number of unique clicks for the campaign""" uniqueClicks: Int! @@ -180,6 +187,7 @@ type Query { emailCampaign(id: ID!): EmailCampaign! emailCampaigns(scope: EmailCampaignContentScopeInput!, search: String, filter: EmailCampaignFilter, sort: [EmailCampaignSort!], offset: Int! = 0, limit: Int! = 25): PaginatedEmailCampaigns! emailCampaignStatistics(id: ID!): BrevoApiCampaignStatistics + senders: [BrevoApiSender!] brevoConfig(scope: EmailCampaignContentScopeInput!): BrevoConfig } diff --git a/packages/api/src/brevo-api/brevo-api-sender.service.ts b/packages/api/src/brevo-api/brevo-api-sender.service.ts new file mode 100644 index 00000000..14ce1746 --- /dev/null +++ b/packages/api/src/brevo-api/brevo-api-sender.service.ts @@ -0,0 +1,26 @@ +import { Inject, Injectable } from "@nestjs/common"; +import * as SibApiV3Sdk from "@sendinblue/client"; + +import { BrevoModuleConfig } from "../config/brevo-module.config"; +import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants"; +import { BrevoApiSender } from "./dto/brevo-api-sender"; + +@Injectable() +export class BrevoApiSenderService { + private readonly senderApi: SibApiV3Sdk.SendersApi; + + constructor(@Inject(BREVO_MODULE_CONFIG) private readonly config: BrevoModuleConfig) { + this.senderApi = new SibApiV3Sdk.SendersApi(); + this.senderApi.setApiKey(SibApiV3Sdk.SendersApiApiKeys.apiKey, config.brevo.apiKey); + } + + public async getSenders(): Promise | undefined> { + const { response, body } = await this.senderApi.getSenders(); + + if (response.statusCode !== 200) { + throw new Error("Failed to get senders"); + } + + return body.senders as BrevoApiSender[]; + } +} diff --git a/packages/api/src/brevo-api/brevo-api.module.ts b/packages/api/src/brevo-api/brevo-api.module.ts index 11159330..cd8e9cfc 100644 --- a/packages/api/src/brevo-api/brevo-api.module.ts +++ b/packages/api/src/brevo-api/brevo-api.module.ts @@ -3,10 +3,11 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "../config/config.module"; import { BrevoApiCampaignsService } from "./brevo-api-campaigns.service"; import { BrevoApiContactsService } from "./brevo-api-contact.service"; +import { BrevoApiSenderService } from "./brevo-api-sender.service"; @Module({ imports: [ConfigModule], - providers: [BrevoApiContactsService, BrevoApiCampaignsService], - exports: [BrevoApiContactsService, BrevoApiCampaignsService], + providers: [BrevoApiContactsService, BrevoApiCampaignsService, BrevoApiSenderService], + exports: [BrevoApiContactsService, BrevoApiCampaignsService, BrevoApiSenderService], }) export class BrevoApiModule {} diff --git a/packages/api/src/brevo-api/dto/brevo-api-sender.ts b/packages/api/src/brevo-api/dto/brevo-api-sender.ts new file mode 100644 index 00000000..98f87cd1 --- /dev/null +++ b/packages/api/src/brevo-api/dto/brevo-api-sender.ts @@ -0,0 +1,18 @@ +import { Field, ID, ObjectType } from "@nestjs/graphql"; + +@ObjectType() +export class BrevoApiSender { + @Field(() => ID) + id: number; + + @Field(() => String) + name: string; + + @Field(() => String) + email: string; + + @Field(() => Boolean) + active: boolean; + + // TODO: add ips +} diff --git a/packages/api/src/brevo-config/brevo-config.module.ts b/packages/api/src/brevo-config/brevo-config.module.ts index 3ceab913..aa3363af 100644 --- a/packages/api/src/brevo-config/brevo-config.module.ts +++ b/packages/api/src/brevo-config/brevo-config.module.ts @@ -1,6 +1,7 @@ import { MikroOrmModule } from "@mikro-orm/nestjs"; import { DynamicModule, Module, Type } from "@nestjs/common"; +import { BrevoApiModule } from "../brevo-api/brevo-api.module"; import { EmailCampaignScopeInterface } from "../types"; import { createBrevoConfigResolver } from "./brevo-config.resolver"; import { BrevoConfigInterface } from "./entities/brevo-config-entity.factory"; @@ -17,7 +18,7 @@ export class BrevoConfigModule { return { module: BrevoConfigModule, - imports: [MikroOrmModule.forFeature([BrevoConfig])], + imports: [MikroOrmModule.forFeature([BrevoConfig]), BrevoApiModule], providers: [BrevoConfigResolver], }; } diff --git a/packages/api/src/brevo-config/brevo-config.resolver.ts b/packages/api/src/brevo-config/brevo-config.resolver.ts index ca2dff3a..081f899a 100644 --- a/packages/api/src/brevo-config/brevo-config.resolver.ts +++ b/packages/api/src/brevo-config/brevo-config.resolver.ts @@ -4,6 +4,8 @@ import { InjectRepository } from "@mikro-orm/nestjs"; import { Type } from "@nestjs/common"; import { Args, ID, Mutation, Query, Resolver } from "@nestjs/graphql"; +import { BrevoApiSenderService } from "../brevo-api/brevo-api-sender.service"; +import { BrevoApiSender } from "../brevo-api/dto/brevo-api-sender"; import { EmailCampaignScopeInterface } from "../types"; import { DynamicDtoValidationPipe } from "../validation/dynamic-dto-validation.pipe"; import { BrevoConfigInput, BrevoConfigUpdateInput } from "./dto/brevo-config.input"; @@ -21,9 +23,17 @@ export function createBrevoConfigResolver({ class BrevoConfigResolver { constructor( private readonly entityManager: EntityManager, + private readonly brevoSenderApiService: BrevoApiSenderService, @InjectRepository(BrevoConfig) private readonly repository: EntityRepository, ) {} + @RequiredPermission(["brevo-newsletter-config"], { skipScopeCheck: true }) + @Query(() => [BrevoApiSender], { nullable: true }) + async senders(): Promise | undefined> { + const senders = await this.brevoSenderApiService.getSenders(); + return senders; + } + @Query(() => BrevoConfig, { nullable: true }) async brevoConfig( @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) From 619784ed8fd9b9070ece1252d85e9eb9ccdfc68d Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Tue, 4 Jun 2024 08:26:11 +0200 Subject: [PATCH 06/13] Add check if brevo config is set in send manager --- .../emailCampaigns/form/EmailCampaignForm.tsx | 17 +++++--- .../form/SendManagerWrapper.gql.ts | 9 ++++ .../form/SendManagerWrapper.tsx | 43 +++++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 packages/admin/src/emailCampaigns/form/SendManagerWrapper.gql.ts create mode 100644 packages/admin/src/emailCampaigns/form/SendManagerWrapper.tsx diff --git a/packages/admin/src/emailCampaigns/form/EmailCampaignForm.tsx b/packages/admin/src/emailCampaigns/form/EmailCampaignForm.tsx index 7ea120b9..a65397b0 100644 --- a/packages/admin/src/emailCampaigns/form/EmailCampaignForm.tsx +++ b/packages/admin/src/emailCampaigns/form/EmailCampaignForm.tsx @@ -52,6 +52,7 @@ import { GQLUpdateEmailCampaignMutationVariables, } from "./EmailCampaignForm.gql.generated"; import { SendManagerFields } from "./SendManagerFields"; +import { SendManagerWrapper } from "./SendManagerWrapper"; import { TestEmailCampaignForm } from "./TestEmailCampaignForm"; interface FormProps { @@ -286,13 +287,15 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe scheduledAt: state.scheduledAt, }} > - - + + + + ), }, diff --git a/packages/admin/src/emailCampaigns/form/SendManagerWrapper.gql.ts b/packages/admin/src/emailCampaigns/form/SendManagerWrapper.gql.ts new file mode 100644 index 00000000..720e9444 --- /dev/null +++ b/packages/admin/src/emailCampaigns/form/SendManagerWrapper.gql.ts @@ -0,0 +1,9 @@ +import { gql } from "@apollo/client"; + +export const brevoConfigQuery = gql` + query BrevoConfig($scope: EmailCampaignContentScopeInput!) { + brevoConfig(scope: $scope) { + id + } + } +`; diff --git a/packages/admin/src/emailCampaigns/form/SendManagerWrapper.tsx b/packages/admin/src/emailCampaigns/form/SendManagerWrapper.tsx new file mode 100644 index 00000000..511e5519 --- /dev/null +++ b/packages/admin/src/emailCampaigns/form/SendManagerWrapper.tsx @@ -0,0 +1,43 @@ +import { useQuery } from "@apollo/client"; +import { Loading } from "@comet/admin"; +import { ContentScopeInterface } from "@comet/cms-admin"; +import { Typography } from "@mui/material"; +import * as React from "react"; +import { FormattedMessage } from "react-intl"; + +import { brevoConfigQuery } from "./SendManagerWrapper.gql"; +import { GQLBrevoConfigQuery, GQLBrevoConfigQueryVariables } from "./SendManagerWrapper.gql.generated"; + +interface SendManagerWrapperProps { + scope: ContentScopeInterface; +} + +export const SendManagerWrapper = ({ scope, children }: React.PropsWithChildren) => { + const { + data: brevoConfig, + loading, + error, + } = useQuery(brevoConfigQuery, { + variables: { scope }, + fetchPolicy: "network-only", + }); + + if (loading) { + return ; + } + + if (error) throw error; + + if (brevoConfig?.brevoConfig?.id == undefined) { + return ( + + + + ); + } + + return <> {children} ; +}; From 44fcc6c1d2593f6a9153ddb7b09bef8305c7ccc9 Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Tue, 4 Jun 2024 09:22:51 +0200 Subject: [PATCH 07/13] Generate changeset --- .changeset/yellow-toes-tickle.md | 20 ++++++++++++++++++++ README.md | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .changeset/yellow-toes-tickle.md diff --git a/.changeset/yellow-toes-tickle.md b/.changeset/yellow-toes-tickle.md new file mode 100644 index 00000000..4237fbb3 --- /dev/null +++ b/.changeset/yellow-toes-tickle.md @@ -0,0 +1,20 @@ +--- +"@comet/brevo-admin": major +"@comet/brevo-api": minor +--- + +A required brevo config page must now be generated with `createBrevoConfigPage`. +All necessary brevo configuration (for each scope) must be configured within this page for emails campaigns to be sent. + +```diff ++ const BrevoConfigPage = createBrevoConfigPage({ ++ scopeParts: ["domain", "language"], ++ }); +``` + +Env vars containing the brevo sender information can be removed. + +```diff +- BREVO_SENDER_NAME=senderName +- BREVO_SENDER_EMAIL=senderEmail +``` diff --git a/README.md b/README.md index 816148e6..4eb31d1d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The following variables must be set manually - `CAMPAIGN_BASIC_AUTH_USERNAME` - `CAMPAIGN_BASIC_AUTH_PASSWORD` -### Configure brevo sender in the demo admin +### Configure brevo sender in the demo admin brevo config page - Brevo sender name - Brevo sender mail From 1baecb7b4c0bbd6a6c5002603a4e7c9b4ae0005d Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Tue, 4 Jun 2024 09:51:21 +0200 Subject: [PATCH 08/13] Add checkConflict check in brevo config form --- .../brevoConfiguration/BrevoConfigForm.tsx | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx index 9f95bb28..c077067c 100644 --- a/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx +++ b/packages/admin/src/brevoConfiguration/BrevoConfigForm.tsx @@ -1,4 +1,4 @@ -import { useApolloClient, useQuery } from "@apollo/client"; +import { gql, useApolloClient, useQuery } from "@apollo/client"; import { Field, FinalForm, @@ -14,7 +14,7 @@ import { useFormApiRef, useStackSwitchApi, } from "@comet/admin"; -import { ContentScopeInterface, EditPageLayout, useFormSaveConflict } from "@comet/cms-admin"; +import { ContentScopeInterface, EditPageLayout, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; import { FormApi } from "final-form"; import React from "react"; import { FormattedMessage } from "react-intl"; @@ -80,10 +80,20 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { const saveConflict = useFormSaveConflict({ checkConflict: async () => { - // TODO: - return false; - // const updatedAt = await queryUpdatedAt(client, "brevoConfig", scope); - // return resolveHasSaveConflict(data?.brevoConfig?.updatedAt, updatedAt); + const query = gql` + query ($scope: EmailCampaignContentScopeInput!) { + brevoConfig(scope: $scope) { + updatedAt + } + } + `; + const { data } = await client.query({ + query, + variables: { scope }, + fetchPolicy: "no-cache", + }); + + return resolveHasSaveConflict(data?.brevoConfig?.updatedAt, data.updatedAt); }, formApiRef, loadLatestVersion: async () => { @@ -161,6 +171,7 @@ export function BrevoConfigForm({ scope }: FormProps): React.ReactElement { name="sender" label={} fullWidth + required /> From b052dd71b6c186748ff69098bebff2f18474a338 Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Tue, 4 Jun 2024 10:02:06 +0200 Subject: [PATCH 09/13] Validate if sender in input is a sender in brevo --- demo/api/schema.gql | 4 ++-- packages/api/schema.gql | 4 ++-- .../src/brevo-config/brevo-config.resolver.ts | 20 +++++++++++++++++-- .../brevo-config/dto/brevo-config.input.ts | 3 +-- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/demo/api/schema.gql b/demo/api/schema.gql index fccdb42a..11a67f26 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -984,6 +984,6 @@ input BrevoConfigInput { } input BrevoConfigUpdateInput { - senderMail: String - senderName: String + senderMail: String! + senderName: String! } diff --git a/packages/api/schema.gql b/packages/api/schema.gql index 4b515ae2..1da55873 100644 --- a/packages/api/schema.gql +++ b/packages/api/schema.gql @@ -329,6 +329,6 @@ input BrevoConfigInput { } input BrevoConfigUpdateInput { - senderMail: String - senderName: String + senderMail: String! + senderName: String! } diff --git a/packages/api/src/brevo-config/brevo-config.resolver.ts b/packages/api/src/brevo-config/brevo-config.resolver.ts index 081f899a..d807b510 100644 --- a/packages/api/src/brevo-config/brevo-config.resolver.ts +++ b/packages/api/src/brevo-config/brevo-config.resolver.ts @@ -27,6 +27,16 @@ export function createBrevoConfigResolver({ @InjectRepository(BrevoConfig) private readonly repository: EntityRepository, ) {} + private async validateSender({ email, name }: { email: string; name: string }): Promise { + const senders = await this.brevoSenderApiService.getSenders(); + + if (!senders || !senders.some((sender) => sender.email === email && sender.name === name)) { + return false; + } + + return true; + } + @RequiredPermission(["brevo-newsletter-config"], { skipScopeCheck: true }) @Query(() => [BrevoApiSender], { nullable: true }) async senders(): Promise | undefined> { @@ -43,14 +53,16 @@ export function createBrevoConfigResolver({ return brevoConfig; } - // TODO: add validation if the input contains a valid sender - @Mutation(() => BrevoConfig) async createBrevoConfig( @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope, @Args("input", { type: () => BrevoConfigInput }) input: BrevoConfigInput, ): Promise { + if (!(await this.validateSender({ email: input.senderMail, name: input.senderName }))) { + throw new Error("Sender not found"); + } + const brevoConfig = this.repository.create({ ...input, scope, @@ -73,6 +85,10 @@ export function createBrevoConfigResolver({ validateNotModified(brevoConfig, lastUpdatedAt); } + if (!(await this.validateSender({ email: input.senderMail, name: input.senderName }))) { + throw new Error("Sender not found"); + } + wrap(brevoConfig).assign({ ...input, }); diff --git a/packages/api/src/brevo-config/dto/brevo-config.input.ts b/packages/api/src/brevo-config/dto/brevo-config.input.ts index 1ceffdc5..ebb63908 100644 --- a/packages/api/src/brevo-config/dto/brevo-config.input.ts +++ b/packages/api/src/brevo-config/dto/brevo-config.input.ts @@ -1,4 +1,3 @@ -import { PartialType } from "@comet/cms-api"; import { Field, InputType } from "@nestjs/graphql"; import { IsEmail, IsNotEmpty, IsString } from "class-validator"; @@ -17,4 +16,4 @@ export class BrevoConfigInput { } @InputType() -export class BrevoConfigUpdateInput extends PartialType(BrevoConfigInput) {} +export class BrevoConfigUpdateInput extends BrevoConfigInput {} From 34b7098962ea9c3214776c1976a72fe0d8046e6a Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Tue, 4 Jun 2024 10:07:31 +0200 Subject: [PATCH 10/13] Add all brevo api sender properties --- packages/api/schema.gql | 7 +++++++ .../api/src/brevo-api/dto/brevo-api-sender.ts | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/api/schema.gql b/packages/api/schema.gql index 1da55873..b0913494 100644 --- a/packages/api/schema.gql +++ b/packages/api/schema.gql @@ -49,6 +49,13 @@ type BrevoApiSender { name: String! email: String! active: Boolean! + ips: [BrevoIp!] +} + +type BrevoIp { + ip: String! + domain: String! + weight: Int! } type BrevoApiCampaignStatistics { diff --git a/packages/api/src/brevo-api/dto/brevo-api-sender.ts b/packages/api/src/brevo-api/dto/brevo-api-sender.ts index 98f87cd1..a8048464 100644 --- a/packages/api/src/brevo-api/dto/brevo-api-sender.ts +++ b/packages/api/src/brevo-api/dto/brevo-api-sender.ts @@ -1,4 +1,4 @@ -import { Field, ID, ObjectType } from "@nestjs/graphql"; +import { Field, ID, Int, ObjectType } from "@nestjs/graphql"; @ObjectType() export class BrevoApiSender { @@ -14,5 +14,18 @@ export class BrevoApiSender { @Field(() => Boolean) active: boolean; - // TODO: add ips + @Field(() => Array(BrevoIp), { nullable: true }) + ips?: Array; +} + +@ObjectType() +class BrevoIp { + @Field(() => String) + ip: string; + + @Field(() => String) + domain: string; + + @Field(() => Int) + weight: number; } From 78c192462e6f79434e8b647d6680e52b4d036172 Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Mon, 10 Jun 2024 14:24:55 +0200 Subject: [PATCH 11/13] Regenerate schema --- demo/api/schema.gql | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/demo/api/schema.gql b/demo/api/schema.gql index 11a67f26..404c0b57 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -168,6 +168,13 @@ type BrevoApiSender { name: String! email: String! active: Boolean! + ips: [BrevoIp!] +} + +type BrevoIp { + ip: String! + domain: String! + weight: Int! } type BrevoApiCampaignStatistics { From ddf4b20fa1d11ec982c3cb52e9f272e32b8116f5 Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Mon, 10 Jun 2024 14:25:42 +0200 Subject: [PATCH 12/13] Check for valid sender defensively --- packages/api/src/brevo-config/brevo-config.resolver.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/api/src/brevo-config/brevo-config.resolver.ts b/packages/api/src/brevo-config/brevo-config.resolver.ts index d807b510..e07f3e8c 100644 --- a/packages/api/src/brevo-config/brevo-config.resolver.ts +++ b/packages/api/src/brevo-config/brevo-config.resolver.ts @@ -30,11 +30,11 @@ export function createBrevoConfigResolver({ private async validateSender({ email, name }: { email: string; name: string }): Promise { const senders = await this.brevoSenderApiService.getSenders(); - if (!senders || !senders.some((sender) => sender.email === email && sender.name === name)) { - return false; + if (senders && senders.some((sender) => sender.email === email && sender.name === name)) { + return true; } - return true; + return false; } @RequiredPermission(["brevo-newsletter-config"], { skipScopeCheck: true }) From 85be9dae12893bce319032792f9ce6eca8e67091 Mon Sep 17 00:00:00 2001 From: Denise Buder Date: Mon, 10 Jun 2024 14:26:54 +0200 Subject: [PATCH 13/13] Rename function validateSender to isValidSender --- packages/api/src/brevo-config/brevo-config.resolver.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/api/src/brevo-config/brevo-config.resolver.ts b/packages/api/src/brevo-config/brevo-config.resolver.ts index e07f3e8c..c587cf24 100644 --- a/packages/api/src/brevo-config/brevo-config.resolver.ts +++ b/packages/api/src/brevo-config/brevo-config.resolver.ts @@ -27,7 +27,7 @@ export function createBrevoConfigResolver({ @InjectRepository(BrevoConfig) private readonly repository: EntityRepository, ) {} - private async validateSender({ email, name }: { email: string; name: string }): Promise { + private async isValidSender({ email, name }: { email: string; name: string }): Promise { const senders = await this.brevoSenderApiService.getSenders(); if (senders && senders.some((sender) => sender.email === email && sender.name === name)) { @@ -59,7 +59,7 @@ export function createBrevoConfigResolver({ scope: typeof Scope, @Args("input", { type: () => BrevoConfigInput }) input: BrevoConfigInput, ): Promise { - if (!(await this.validateSender({ email: input.senderMail, name: input.senderName }))) { + if (!(await this.isValidSender({ email: input.senderMail, name: input.senderName }))) { throw new Error("Sender not found"); } @@ -85,7 +85,7 @@ export function createBrevoConfigResolver({ validateNotModified(brevoConfig, lastUpdatedAt); } - if (!(await this.validateSender({ email: input.senderMail, name: input.senderName }))) { + if (!(await this.isValidSender({ email: input.senderMail, name: input.senderName }))) { throw new Error("Sender not found"); }