Skip to content

Commit

Permalink
Make it possible to select multiple target groups for one campaign (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasdax98 authored Aug 20, 2024
1 parent a7b82e7 commit f7c0fdd
Show file tree
Hide file tree
Showing 17 changed files with 148 additions and 94 deletions.
6 changes: 6 additions & 0 deletions .changeset/tall-berries-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@comet/brevo-admin": minor
"@comet/brevo-api": minor
---

Allow sending a campaign to multiple target groups
6 changes: 3 additions & 3 deletions demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ type EmailCampaign implements DocumentInterface {
subject: String!
brevoId: Int
scheduledAt: DateTime
targetGroup: TargetGroup
targetGroups: [TargetGroup!]!
content: EmailCampaignContentBlockData!
scope: EmailCampaignContentScope!
sendingState: SendingState!
Expand Down Expand Up @@ -896,7 +896,7 @@ input EmailCampaignInput {
title: String!
subject: String!
scheduledAt: DateTime
targetGroup: ID
targetGroups: [ID!]!
content: EmailCampaignContentBlockInput!
}

Expand All @@ -907,7 +907,7 @@ input EmailCampaignUpdateInput {
title: String
subject: String
scheduledAt: DateTime
targetGroup: ID
targetGroups: [ID!]
content: EmailCampaignContentBlockInput
}

Expand Down
11 changes: 6 additions & 5 deletions packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const emailCampaignsFragment = gql`
scheduledAt
brevoId
content
targetGroup {
targetGroups {
id
title
}
Expand Down Expand Up @@ -155,10 +155,10 @@ export function EmailCampaignsGrid({
width: 200,
},
{
field: "targetGroup",
headerName: intl.formatMessage({ id: "cometBrevoModule.emailCampaign.targetGroup", defaultMessage: "Target group" }),
field: "targetGroups",
headerName: intl.formatMessage({ id: "cometBrevoModule.emailCampaign.targetGroups", defaultMessage: "Target groups" }),
width: 150,
renderCell: ({ value }) => (value ? value.title : "-"),
renderCell: ({ value }) => value.map((value: { title: string }) => value.title).join(", "),
filterable: false,
sortable: false,
},
Expand Down Expand Up @@ -195,12 +195,13 @@ export function EmailCampaignsGrid({
title: row.title,
subject: row.subject,
content: EmailCampaignContentBlock.state2Output(EmailCampaignContentBlock.input2State(row.content)),
targetGroups: row.targetGroups.map((targetGroup) => targetGroup.id),
};
}}
onPaste={async ({ input }) => {
await client.mutate<GQLCreateEmailCampaignMutation, GQLCreateEmailCampaignMutationVariables>({
mutation: createEmailCampaignMutation,
variables: { scope, input },
variables: { scope, input: { ...input } },
});
}}
onDelete={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export const emailCampaignFormFragment = gql`
scheduledAt
content
sendingState
targetGroup {
targetGroups {
id
title
}
}
`;
Expand Down
23 changes: 13 additions & 10 deletions packages/admin/src/emailCampaigns/form/EmailCampaignForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe
content: EmailCampaignContentBlock,
};

type EmailCampaignState = Omit<GQLEmailCampaignFormFragment, "content" | "targetGroup"> & {
type EmailCampaignState = Omit<GQLEmailCampaignFormFragment, "content"> & {
[key in keyof typeof rootBlocks]: BlockState<(typeof rootBlocks)[key]>;
} & { targetGroup?: string };
};

const stackApi = useStackApi();
const stackSwitchApi = useStackSwitchApi();
Expand Down Expand Up @@ -103,21 +103,23 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe
content: EmailCampaignContentBlock.input2State(emailCampaign.content),
scheduledAt: emailCampaign?.scheduledAt ? new Date(emailCampaign.scheduledAt) : null,
sendingState: emailCampaign?.sendingState,
targetGroup: emailCampaign?.targetGroup?.id,
targetGroups: emailCampaign?.targetGroups,
};
},
state2Output: (state) => ({
...state,
content: EmailCampaignContentBlock.state2Output(state.content),
scheduledAt: state.targetGroup ? state.scheduledAt ?? null : null,
scheduledAt: state.targetGroups.length > 0 ? state.scheduledAt ?? null : null,
sendingState: undefined,
targetGroups: state.targetGroups.map((targetGroup) => targetGroup.id),
}),
defaultState: {
title: "",
subject: "",
content: EmailCampaignContentBlock.defaultValues(),
sendingState: "DRAFT",
scheduledAt: undefined,
targetGroups: [],
},
});

Expand Down Expand Up @@ -168,6 +170,7 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe
if (!id) {
throw new Error("Missing id in edit mode");
}

const { data: mutationResponse } = await client.mutate<GQLUpdateEmailCampaignMutation, GQLUpdateEmailCampaignMutationVariables>({
mutation: updateEmailCampaignMutation,
variables: { id, input: { ...output }, lastUpdatedAt: query.data?.emailCampaign?.updatedAt },
Expand All @@ -181,7 +184,7 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe
} else {
const { data: mutationResponse } = await client.mutate<GQLCreateEmailCampaignMutation, GQLCreateEmailCampaignMutationVariables>({
mutation: createEmailCampaignMutation,
variables: { scope, input: { ...output, targetGroup: output.targetGroup } },
variables: { scope, input: { ...output, targetGroups: output.targetGroups } },
});

if (!mutationResponse) {
Expand Down Expand Up @@ -215,7 +218,7 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe
};

const isScheduledDateInPast = state.scheduledAt != undefined && isBefore(new Date(state.scheduledAt), new Date());
const isSchedulingDisabled = state.sendingState === "SENT" || mode === "add" || !state.targetGroup || isScheduledDateInPast;
const isSchedulingDisabled = state.sendingState === "SENT" || mode === "add" || state.targetGroups.length === 0 || isScheduledDateInPast;

return (
<EditPageLayout>
Expand Down Expand Up @@ -283,19 +286,19 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe
),
content: (
<BlocksFinalForm
onSubmit={(values) => setState({ ...state, scheduledAt: values.scheduledAt, targetGroup: values.targetGroup })}
onSubmit={(values) => setState({ ...state, scheduledAt: values.scheduledAt, targetGroups: values.targetGroups })}
initialValues={{
targetGroup: state.targetGroup,
targetGroups: state.targetGroups,
scheduledAt: state.scheduledAt,
}}
>
<SendManagerFields
scope={scope}
isSchedulingDisabled={isSchedulingDisabled}
isSendable={!hasChanges && state.targetGroup != undefined}
isSendable={!hasChanges && state.targetGroups != undefined}
id={id}
/>
<TestEmailCampaignForm id={id} isSendable={!hasChanges && state.targetGroup != undefined} />
<TestEmailCampaignForm id={id} isSendable={!hasChanges && state.targetGroups != undefined} />
</BlocksFinalForm>
),
},
Expand Down
14 changes: 11 additions & 3 deletions packages/admin/src/emailCampaigns/form/SendManagerFields.gql.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { gql } from "@apollo/client";

const targetGroupSelectFragment = gql`
fragment TargetGroupSelect on TargetGroup {
id
title
}
`;

export const targetGroupsSelectQuery = gql`
query TargetGroupsSelect($scope: EmailCampaignContentScopeInput!) {
targetGroups(scope: $scope) {
targetGroups(scope: $scope, limit: 100) {
nodes {
id
title
...TargetGroupSelect
}
}
}
${targetGroupSelectFragment}
`;

export const sendEmailCampaignNowMutation = gql`
Expand Down
43 changes: 24 additions & 19 deletions packages/admin/src/emailCampaigns/form/SendManagerFields.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useMutation, useQuery } from "@apollo/client";
import { Field, FinalFormSelect, SaveButton, useStackSwitchApi } from "@comet/admin";
import { useApolloClient, useMutation } from "@apollo/client";
import { Field, FinalFormSelect, SaveButton, useAsyncOptionsProps, useStackSwitchApi } from "@comet/admin";
import { FinalFormDateTimePicker } from "@comet/admin-date-time";
import { Newsletter } from "@comet/admin-icons";
import { AdminComponentPaper, AdminComponentSectionGroup } from "@comet/blocks-admin";
import { ContentScopeInterface } from "@comet/cms-admin";
import { Card, MenuItem } from "@mui/material";
import { Card } from "@mui/material";
import * as React from "react";
import { FormattedMessage } from "react-intl";

Expand All @@ -13,6 +13,7 @@ import { sendEmailCampaignNowMutation, targetGroupsSelectQuery } from "./SendMan
import {
GQLSendEmailCampaignNowMutation,
GQLSendEmailCampaignNowMutationVariables,
GQLTargetGroupSelectFragment,
GQLTargetGroupsSelectQuery,
GQLTargetGroupsSelectQueryVariables,
} from "./SendManagerFields.gql.generated";
Expand All @@ -39,12 +40,18 @@ const validateScheduledAt = (value: Date, now: Date) => {

export const SendManagerFields = ({ isSchedulingDisabled, scope, id, isSendable }: SendManagerFieldsProps) => {
const stackSwitchApi = useStackSwitchApi();
const apolloClient = useApolloClient();

const [isSendEmailCampaignNowDialogOpen, setIsSendEmailCampaignNowDialogOpen] = React.useState(false);

const { data: targetGroups } = useQuery<GQLTargetGroupsSelectQuery, GQLTargetGroupsSelectQueryVariables>(targetGroupsSelectQuery, {
variables: { scope },
fetchPolicy: "network-only",
const selectAsyncMultipleProps = useAsyncOptionsProps(async () => {
return (
await apolloClient.query<GQLTargetGroupsSelectQuery, GQLTargetGroupsSelectQueryVariables>({
query: targetGroupsSelectQuery,
variables: { scope },
fetchPolicy: "network-only",
})
).data.targetGroups.nodes;
});

const [sendEmailCampaignNow, { loading: sendEmailCampaignNowLoading, error: sendEmailCampaignNowError }] = useMutation<
Expand All @@ -69,21 +76,19 @@ export const SendManagerFields = ({ isSchedulingDisabled, scope, id, isSendable
validate={(value) => (isSchedulingDisabled ? undefined : validateScheduledAt(value, now))}
componentsProps={{ datePicker: { placeholder: "DD.MM.YYYY", minDate: now }, timePicker: { placeholder: "HH:mm" } }}
/>

<Field
label={<FormattedMessage id="cometBrevoModule.emailCampaigns.targetGroup" defaultMessage="Target group" />}
name="targetGroup"
component={FinalFormSelect}
getOptionLabel={(option: GQLTargetGroupSelectFragment) => option.title}
getOptionSelected={(option: GQLTargetGroupSelectFragment, value: string) => {
return option.id === value;
}}
{...selectAsyncMultipleProps}
name="targetGroups"
label={<FormattedMessage id="cometBrevoModule.emailCampaigns.targetGroups" defaultMessage="Target groups" />}
multiple
fullWidth
>
{(props) => (
<FinalFormSelect {...props} fullWidth clearable>
{targetGroups?.targetGroups.nodes.map((option) => (
<MenuItem value={option.id} key={option.id}>
{option.title}
</MenuItem>
))}
</FinalFormSelect>
)}
</Field>
/>

<SaveButton
variant="contained"
Expand Down
2 changes: 1 addition & 1 deletion packages/api/generate-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ async function generateSchema(): Promise<void> {
const gqlSchemaFactory = app.get(GraphQLSchemaFactory);

const BrevoContact = BrevoContactFactory.create({});
const [BrevoContactInput, BrevoContactUpdateInput] = BrevoContactInputFactory.create({});
const [BrevoContactInput, BrevoContactUpdateInput] = BrevoContactInputFactory.create({ Scope: EmailCampaignScope });
const BrevoContactSubscribeInput = SubscribeInputFactory.create({ Scope: EmailCampaignScope });
const BrevoContactResolver = createBrevoContactResolver({
BrevoContact,
Expand Down
6 changes: 3 additions & 3 deletions packages/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ type EmailCampaign implements DocumentInterface {
subject: String!
brevoId: Int
scheduledAt: DateTime
targetGroup: TargetGroup
targetGroups: [TargetGroup!]!
content: EmailCampaignContentBlockData!
scope: EmailCampaignContentScope!
sendingState: SendingState!
Expand Down Expand Up @@ -303,7 +303,7 @@ input EmailCampaignInput {
title: String!
subject: String!
scheduledAt: DateTime
targetGroup: ID
targetGroups: [ID!]!
content: EmailCampaignContentBlockInput!
}

Expand All @@ -314,7 +314,7 @@ input EmailCampaignUpdateInput {
title: String
subject: String
scheduledAt: DateTime
targetGroup: ID
targetGroups: [ID!]
content: EmailCampaignContentBlockInput
}

Expand Down
8 changes: 4 additions & 4 deletions packages/api/src/brevo-api/brevo-api-campaigns.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ export class BrevoApiCampaignsService {
htmlContent: string;
scheduledAt?: Date;
}): Promise<number> {
const targetGroup = await campaign.targetGroup?.load();
const targetGroups = await campaign.targetGroups.loadItems();
const { sender } = this.config.brevo.resolveConfig(campaign.scope);

const emailCampaign = {
name: campaign.title,
subject: campaign.subject,
sender: { name: sender.name, email: sender.email },
recipients: { listIds: targetGroup ? [targetGroup?.brevoId] : [] },
recipients: { listIds: targetGroups.map((targetGroup) => targetGroup.brevoId) },
htmlContent,
scheduledAt: scheduledAt?.toISOString(),
};
Expand All @@ -81,14 +81,14 @@ export class BrevoApiCampaignsService {
htmlContent: string;
scheduledAt?: Date;
}): Promise<boolean> {
const targetGroup = await campaign.targetGroup?.load();
const targetGroups = await campaign.targetGroups.loadItems();
const { sender } = this.config.brevo.resolveConfig(campaign.scope);

const emailCampaign = {
name: campaign.title,
subject: campaign.subject,
sender: { name: sender.name, mail: sender.email },
recipients: { listIds: targetGroup ? [targetGroup?.brevoId] : [] },
recipients: { listIds: targetGroups.map((targetGroup) => targetGroup.brevoId) },
htmlContent,
scheduledAt: scheduledAt?.toISOString(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface EmailCampaignInputInterface {
subject: string;
scheduledAt?: Date | null;
content: BlockInputInterface;
targetGroup?: string;
targetGroups: string[];
}

export class EmailCampaignInputFactory {
Expand Down Expand Up @@ -38,10 +38,9 @@ export class EmailCampaignInputFactory {
@Field(() => Date, { nullable: true })
scheduledAt?: Date | null;

@IsUndefinable()
@Field(() => ID, { nullable: true })
@IsUUID()
targetGroup?: string;
@Field(() => [ID])
@IsUUID(4, { each: true })
targetGroups: string[];

@Field(() => RootBlockInputScalar(EmailCampaignContentBlock))
@Transform(({ value }) => (isBlockInputInterface(value) ? value : EmailCampaignContentBlock.blockInputFactory(value)), {
Expand Down
Loading

0 comments on commit f7c0fdd

Please sign in to comment.