diff --git a/.changeset/chilly-dogs-draw.md b/.changeset/chilly-dogs-draw.md
new file mode 100644
index 00000000..2366bc52
--- /dev/null
+++ b/.changeset/chilly-dogs-draw.md
@@ -0,0 +1,8 @@
+---
+"@comet/brevo-admin": minor
+"@comet/brevo-api": minor
+---
+
+Adds `createBrevoTestContactsPage` for creating a test contacts page, that is indepent from the main list.
+
+Remove `email` and `redirectionUrl` from `brevoContactsPageAttributesConfig`
diff --git a/demo/admin/src/common/MasterMenu.tsx b/demo/admin/src/common/MasterMenu.tsx
index 12e421c0..2b7b0e47 100644
--- a/demo/admin/src/common/MasterMenu.tsx
+++ b/demo/admin/src/common/MasterMenu.tsx
@@ -1,5 +1,11 @@
import { Assets, Dashboard, Mail, PageTree, Wrench } from "@comet/admin-icons";
-import { BrevoConfigPage, createBrevoContactsPage, createEmailCampaignsPage, createTargetGroupsPage } from "@comet/brevo-admin";
+import {
+ BrevoConfigPage,
+ createBrevoContactsPage,
+ createBrevoTestContactsPage,
+ createEmailCampaignsPage,
+ createTargetGroupsPage,
+} from "@comet/brevo-admin";
import {
AllCategories,
ContentScopeIndicator,
@@ -57,6 +63,14 @@ const getMasterMenuData = ({ brevoContactConfig }: { brevoContactConfig: BrevoCo
EmailCampaignContentBlock,
});
+ const BrevoTestContactsPage = createBrevoTestContactsPage({
+ scopeParts: ["domain", "language"],
+ additionalAttributesFragment: brevoContactConfig.additionalAttributesFragment,
+ additionalGridFields: brevoContactConfig.additionalGridFields,
+ additionalFormFields: brevoContactConfig.additionalFormFields,
+ input2State: brevoContactConfig.input2State,
+ });
+
return [
{
type: "route",
@@ -106,6 +120,14 @@ const getMasterMenuData = ({ brevoContactConfig }: { brevoContactConfig: BrevoCo
render: () => ,
},
},
+ {
+ type: "route",
+ primary: ,
+ route: {
+ path: "/newsletter/test-contacts",
+ render: () => ,
+ },
+ },
{
type: "route",
primary: ,
diff --git a/demo/admin/src/common/brevoModuleConfig/brevoContactsPageAttributesConfig.tsx b/demo/admin/src/common/brevoModuleConfig/brevoContactsPageAttributesConfig.tsx
index d1712c62..c1345369 100644
--- a/demo/admin/src/common/brevoModuleConfig/brevoContactsPageAttributesConfig.tsx
+++ b/demo/admin/src/common/brevoModuleConfig/brevoContactsPageAttributesConfig.tsx
@@ -67,8 +67,6 @@ export interface BrevoContactConfig {
name: string;
};
input2State: (values?: AdditionalFormConfigInputProps) => {
- email: string;
- redirectionUrl: string;
attributes: { BRANCH?: Array; SALUTATION?: GQLBrevoContactSalutation; FIRSTNAME?: string; LASTNAME?: string };
};
exportFields: {
@@ -139,8 +137,6 @@ export const getBrevoContactConfig = (intl: IntlShape): BrevoContactConfig => {
),
input2State: (values?: AdditionalFormConfigInputProps) => {
return {
- email: values?.email ?? "",
- redirectionUrl: values?.redirectionUrl ?? "",
attributes: {
BRANCH: values?.attributes?.BRANCH ?? [],
SALUTATION: values?.attributes?.SALUTATION,
diff --git a/demo/api/schema.gql b/demo/api/schema.gql
index 661327ed..8a0ea14b 100644
--- a/demo/api/schema.gql
+++ b/demo/api/schema.gql
@@ -448,6 +448,7 @@ type TargetGroup implements DocumentInterface {
createdAt: DateTime!
title: String!
isMainList: Boolean!
+ isTestList: Boolean!
brevoId: Int!
totalSubscribers: Int!
scope: EmailCampaignContentScope!
@@ -576,6 +577,7 @@ type Query {
dontUseFileUploadDummy: FileUpload
brevoContact(id: Int!, scope: EmailCampaignContentScopeInput!): BrevoContact!
brevoContacts(targetGroupId: ID, email: String, scope: EmailCampaignContentScopeInput!, offset: Int! = 0, limit: Int! = 25): PaginatedBrevoContacts!
+ brevoTestContacts(targetGroupId: ID, email: String, scope: EmailCampaignContentScopeInput!, offset: Int! = 0, limit: Int! = 25): PaginatedBrevoContacts!
manuallyAssignedBrevoContacts(offset: Int! = 0, limit: Int! = 25, targetGroupId: ID!, email: String): PaginatedBrevoContacts!
emailCampaign(id: ID!): EmailCampaign!
emailCampaigns(scope: EmailCampaignContentScopeInput!, search: String, filter: EmailCampaignFilter, sort: [EmailCampaignSort!], offset: Int! = 0, limit: Int! = 25): PaginatedEmailCampaigns!
@@ -735,6 +737,7 @@ input TargetGroupFilter {
createdAt: DateTimeFilter
updatedAt: DateTimeFilter
title: StringFilter
+ isTestList: BooleanFilter
and: [TargetGroupFilter!]
or: [TargetGroupFilter!]
}
@@ -787,7 +790,9 @@ type Mutation {
deleteDamFolder(id: ID!): Boolean!
updateBrevoContact(id: Int!, scope: EmailCampaignContentScopeInput!, input: BrevoContactUpdateInput!): BrevoContact!
createBrevoContact(scope: EmailCampaignContentScopeInput!, input: BrevoContactInput!): SubscribeResponse!
+ createBrevoTestContact(scope: EmailCampaignContentScopeInput!, input: BrevoTestContactInput!): SubscribeResponse!
deleteBrevoContact(id: Int!, scope: EmailCampaignContentScopeInput!): Boolean!
+ deleteBrevoTestContact(id: Int!, scope: EmailCampaignContentScopeInput!): Boolean!
subscribeBrevoContact(input: SubscribeInput!, scope: EmailCampaignContentScopeInput!): SubscribeResponse!
startBrevoContactImport(fileId: ID!, targetGroupIds: [ID!], scope: EmailCampaignContentScopeInput!): CsvImportInformation!
createEmailCampaign(scope: EmailCampaignContentScopeInput!, input: EmailCampaignInput!): EmailCampaign!
@@ -947,6 +952,12 @@ input BrevoContactInput {
attributes: BrevoContactAttributesInput
}
+input BrevoTestContactInput {
+ email: String!
+ blocked: Boolean!
+ attributes: BrevoContactAttributesInput
+}
+
input SubscribeInput {
email: String!
redirectionUrl: String!
diff --git a/demo/campaign/src/documents/pages/Page.tsx b/demo/campaign/src/documents/pages/Page.tsx
deleted file mode 100644
index 49890fa3..00000000
--- a/demo/campaign/src/documents/pages/Page.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { SeoBlock } from "@src/documents/pages/blocks/SeoBlock";
-import { Layout, PropsWithLayout } from "@src/layout/Layout";
-import { gql } from "graphql-request";
-import Head from "next/head";
-import * as React from "react";
-
-import { PageContentBlock } from "./blocks/PageContentBlock";
-import { GQLPageQuery } from "./Page.generated";
-
-export const pageQuery = gql`
- query Page($pageId: ID!) {
- pageContent: pageTreeNode(id: $pageId) {
- name
- path
- document {
- __typename
- ... on Page {
- content
- seo
- }
- }
- }
- }
-`;
-
-export function Page(props: PropsWithLayout): JSX.Element {
- const document = props.pageContent?.document;
- return (
-
-
-
-
- {document?.__typename === "Page" && (
-
- )}
- {document && document.__typename === "Page" ? : null}
-
- );
-}
diff --git a/demo/campaign/src/documents/pages/blocks/PageContentBlock.tsx b/demo/campaign/src/documents/pages/blocks/PageContentBlock.tsx
deleted file mode 100644
index c571d753..00000000
--- a/demo/campaign/src/documents/pages/blocks/PageContentBlock.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { BlocksBlock, PropsWithData, SupportedBlocks, YouTubeVideoBlock } from "@comet/cms-site";
-import { PageContentBlockData } from "@src/blocks.generated";
-import { DamImageBlock } from "@src/common/blocks/DamImageBlock";
-import { DamVideoBlock } from "@src/common/blocks/DamVideoBlock";
-import { HeadlineBlock } from "@src/common/blocks/HeadlineBlock";
-import { LinkListBlock } from "@src/common/blocks/LinkListBlock";
-import { RichTextBlock } from "@src/common/blocks/RichTextBlock";
-import { SpaceBlock } from "@src/common/blocks/SpaceBlock";
-import { TextImageBlock } from "@src/common/blocks/TextImageBlock";
-import * as React from "react";
-
-const supportedBlocks: SupportedBlocks = {
- space: (props) => ,
- richtext: (props) => ,
- headline: (props) => ,
- image: (props) => ,
- textImage: (props) => ,
- damVideo: (props) => ,
- youTubeVideo: (props) => ,
- links: (props) => ,
-};
-
-export const PageContentBlock: React.FC> = ({ data }) => {
- return ;
-};
diff --git a/demo/campaign/src/documents/pages/blocks/SeoBlock.tsx b/demo/campaign/src/documents/pages/blocks/SeoBlock.tsx
deleted file mode 100644
index 0b395246..00000000
--- a/demo/campaign/src/documents/pages/blocks/SeoBlock.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { generateImageUrl, PropsWithData } from "@comet/cms-site";
-import { SeoBlockData } from "@src/blocks.generated";
-import Head from "next/head";
-import * as React from "react";
-
-interface SeoBlockProps extends PropsWithData {
- title: string;
- canonicalUrl: string;
-}
-export const SeoBlock: React.FunctionComponent = ({
- data: { htmlTitle, metaDescription, openGraphTitle, openGraphDescription, openGraphImage, noIndex },
- title,
- canonicalUrl,
-}) => {
- const usedHtmlTitle = htmlTitle && htmlTitle != "" ? htmlTitle : title;
- return (
-
- {usedHtmlTitle}
-
- {/* Meta*/}
- {metaDescription && }
-
-
- {/* Open Graph */}
- {openGraphTitle && }
- {openGraphDescription && }
-
-
- {openGraphImage.block?.urlTemplate && (
-
- )}
-
- {/* No Index */}
- {noIndex && }
-
- );
-};
diff --git a/package.json b/package.json
index c7a0d688..d81f3ada 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"private": true,
"scripts": {
"build": "pnpm recursive run build",
+ "build:packages": "pnpm run build",
"build:api": "pnpm recursive --filter '@comet/brevo-api' run build",
"build:admin": "pnpm recursive --filter '@comet/brevo-admin' run build",
"build:api:skippable": "test -f packages/api/lib/index.d.ts && echo 'Skipping API build' || $npm_execpath build:api",
diff --git a/packages/admin/src/brevoContacts/form/BrevoContactForm.tsx b/packages/admin/src/brevoContacts/form/BrevoContactForm.tsx
index af1265eb..02ec01bd 100644
--- a/packages/admin/src/brevoContacts/form/BrevoContactForm.tsx
+++ b/packages/admin/src/brevoContacts/form/BrevoContactForm.tsx
@@ -42,9 +42,12 @@ import {
} from "./BrevoContactForm.gql.generated";
export type EditBrevoContactFormValues = {
+ [key: string]: unknown;
+};
+
+type EditBrevoContactFormValuesWithAttributes = EditBrevoContactFormValues & {
email: string;
redirectionUrl: string;
- [key: string]: unknown;
};
interface FormProps {
@@ -59,14 +62,11 @@ export function BrevoContactForm({ id, scope, input2State, additionalFormFields,
const stackApi = useStackApi();
const client = useApolloClient();
const mode = id ? "edit" : "add";
- const formApiRef = useFormApiRef();
+ const formApiRef = useFormApiRef();
const brevoContactFormFragment = gql`
fragment BrevoContactForm on BrevoContact {
email
- createdAt
- emailBlacklisted
- smsBlacklisted
${additionalAttributesFragment ? "...".concat(additionalAttributesFragment?.name) : ""}
}
${additionalAttributesFragment?.fragment ?? ""}
@@ -76,18 +76,20 @@ export function BrevoContactForm({ id, scope, input2State, additionalFormFields,
id ? { variables: { id, scope } } : { skip: true },
);
- const initialValues = React.useMemo>(() => {
- let additionalInitialValues = {};
+ const initialValues = React.useMemo>(() => {
+ let baseInitialValues = {
+ email: "",
+ redirectionUrl: "",
+ };
if (input2State) {
- additionalInitialValues = input2State({ email: "", redirectionUrl: "", ...data?.brevoContact });
+ baseInitialValues = {
+ ...baseInitialValues,
+ ...input2State(data?.brevoContact),
+ };
}
- return data?.brevoContact
- ? {
- email: data.brevoContact.email,
- ...additionalInitialValues,
- }
- : additionalInitialValues;
+
+ return data?.brevoContact?.email ? { ...baseInitialValues, email: data.brevoContact.email } : baseInitialValues;
}, [data?.brevoContact, input2State]);
const saveConflict = useFormSaveConflict({
@@ -112,7 +114,11 @@ export function BrevoContactForm({ id, scope, input2State, additionalFormFields,
},
});
- const handleSubmit = async (state: EditBrevoContactFormValues, form: FormApi, event: FinalFormSubmitEvent) => {
+ const handleSubmit = async (
+ state: EditBrevoContactFormValuesWithAttributes,
+ form: FormApi,
+ event: FinalFormSubmitEvent,
+ ) => {
if (await saveConflict.checkForConflicts()) {
throw new Error("Conflicts detected");
}
@@ -159,7 +165,7 @@ export function BrevoContactForm({ id, scope, input2State, additionalFormFields,
}
return (
- apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}>
+ apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}>
{({ values }) => (
<>
{saveConflict.dialogs}
diff --git a/packages/admin/src/brevoTestContacts/BrevoTestContactsGrid.tsx b/packages/admin/src/brevoTestContacts/BrevoTestContactsGrid.tsx
new file mode 100644
index 00000000..84a82fee
--- /dev/null
+++ b/packages/admin/src/brevoTestContacts/BrevoTestContactsGrid.tsx
@@ -0,0 +1,204 @@
+import { DocumentNode, gql, useApolloClient, useQuery } from "@apollo/client";
+import {
+ Alert,
+ DataGridToolbar,
+ GridColDef,
+ MainContent,
+ messages,
+ RowActionsItem,
+ RowActionsMenu,
+ StackLink,
+ ToolbarActions,
+ ToolbarFillSpace,
+ ToolbarItem,
+ ToolbarTitleItem,
+ useBufferedRowCount,
+ useDataGridRemote,
+ usePersistentColumnState,
+} from "@comet/admin";
+import { Add, Delete, Edit } from "@comet/admin-icons";
+import { ContentScopeInterface } from "@comet/cms-admin";
+import { Box, Button, IconButton } from "@mui/material";
+import { DataGrid, GridToolbarQuickFilter } from "@mui/x-data-grid";
+import * as React from "react";
+import { FormattedMessage, IntlShape, useIntl } from "react-intl";
+
+import { GQLEmailCampaignContentScopeInput } from "../graphql.generated";
+import {
+ GQLBrevoContactsListFragment,
+ GQLBrevoTestContactsGridQuery,
+ GQLBrevoTestContactsGridQueryVariables,
+ GQLDeleteBrevoTestContactMutation,
+ GQLDeleteBrevoTestContactMutationVariables,
+} from "./BrevoTestContactsGrid.generated";
+
+const brevoContactsFragment = gql`
+ fragment BrevoContactsList on BrevoContact {
+ id
+ createdAt
+ modifiedAt
+ email
+ emailBlacklisted
+ smsBlacklisted
+ }
+`;
+
+const deleteBrevoTestContactMutation = gql`
+ mutation DeleteBrevoTestContact($id: Int!, $scope: EmailCampaignContentScopeInput!) {
+ deleteBrevoTestContact(id: $id, scope: $scope)
+ }
+`;
+
+function BrevoTestContactsGridToolbar({ intl, scope }: { intl: IntlShape; scope: GQLEmailCampaignContentScopeInput }) {
+ return (
+
+
+
+
+
+
+
+
+
+ } component={StackLink} pageName="add" payload="add" variant="contained" color="primary">
+
+
+
+
+ );
+}
+
+export function BrevoTestContactsGrid({
+ scope,
+ additionalAttributesFragment,
+ additionalGridFields = [],
+}: {
+ scope: ContentScopeInterface;
+ additionalAttributesFragment?: { name: string; fragment: DocumentNode };
+ additionalGridFields?: GridColDef[];
+}): React.ReactElement {
+ const brevoTestContactsQuery = gql`
+ query BrevoTestContactsGrid($offset: Int, $limit: Int, $email: String, $scope: EmailCampaignContentScopeInput!) {
+ brevoTestContacts(offset: $offset, limit: $limit, email: $email, scope: $scope) {
+ nodes {
+ ...BrevoContactsList
+ ${additionalAttributesFragment ? "...".concat(additionalAttributesFragment?.name) : ""}
+ }
+ totalCount
+ }
+ }
+ ${brevoContactsFragment}
+ ${additionalAttributesFragment?.fragment ?? ""}
+ `;
+ const client = useApolloClient();
+ const intl = useIntl();
+ const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("BrevoContactsGrid") };
+
+ const columns: GridColDef[] = [
+ {
+ field: "createdAt",
+ headerName: intl.formatMessage({ id: "cometBrevoModule.brevoTestContact.subscribedAt", defaultMessage: "Subscribed At" }),
+ filterable: false,
+ sortable: false,
+ width: 150,
+ renderCell: ({ row }) => intl.formatDate(new Date(row.createdAt)),
+ },
+ {
+ field: "modifiedAt",
+ headerName: intl.formatMessage({ id: "cometBrevoModule.brevoTestContact.modifiedAt", defaultMessage: "Modified At" }),
+ filterable: false,
+ sortable: false,
+ width: 150,
+ renderCell: ({ row }) => intl.formatDate(new Date(row.modifiedAt)),
+ },
+ {
+ field: "email",
+ headerName: intl.formatMessage({ id: "cometBrevoModule.brevoTestContact.email", defaultMessage: "Email" }),
+ filterable: false,
+ sortable: false,
+ width: 150,
+ flex: 1,
+ },
+ ...additionalGridFields,
+ {
+ field: "actions",
+ headerName: "",
+ sortable: false,
+ filterable: false,
+ type: "actions",
+ renderCell: (params) => {
+ return (
+ <>
+
+
+
+
+
+ {
+ await client.mutate({
+ mutation: deleteBrevoTestContactMutation,
+ variables: { id: params.row.id, scope },
+ refetchQueries: [brevoTestContactsQuery],
+ });
+ }}
+ icon={}
+ >
+
+
+
+
+ >
+ );
+ },
+ },
+ ];
+
+ const { data, loading, error } = useQuery(brevoTestContactsQuery, {
+ variables: {
+ offset: dataGridProps.page * dataGridProps.pageSize,
+ limit: dataGridProps.pageSize,
+ email: dataGridProps.filterModel?.quickFilterValues ? dataGridProps.filterModel?.quickFilterValues[0] : undefined,
+ scope,
+ },
+ });
+
+ const rowCount = useBufferedRowCount(data?.brevoTestContacts.totalCount);
+ if (error) throw error;
+ const rows = data?.brevoTestContacts.nodes ?? [];
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/admin/src/brevoTestContacts/BrevoTestContactsPage.tsx b/packages/admin/src/brevoTestContacts/BrevoTestContactsPage.tsx
new file mode 100644
index 00000000..f961de5a
--- /dev/null
+++ b/packages/admin/src/brevoTestContacts/BrevoTestContactsPage.tsx
@@ -0,0 +1,78 @@
+import { GridColDef, Stack, StackPage, StackSwitch, StackToolbar } from "@comet/admin";
+import { ContentScopeIndicator, useContentScope } from "@comet/cms-admin";
+import { DocumentNode } from "graphql";
+import * as React from "react";
+import { useIntl } from "react-intl";
+
+import { BrevoTestContactsGrid } from "./BrevoTestContactsGrid";
+import { BrevoTestContactForm, EditBrevoContactFormValues } from "./form/BrevoTestContactForm";
+
+interface CreateContactsPageOptions {
+ scopeParts: string[];
+ additionalAttributesFragment?: { name: string; fragment: DocumentNode };
+ additionalGridFields?: GridColDef[];
+ additionalFormFields?: React.ReactNode;
+ input2State?: (values?: EditBrevoContactFormValues) => EditBrevoContactFormValues;
+}
+
+function createBrevoTestContactsPage({
+ scopeParts,
+ additionalAttributesFragment,
+ additionalFormFields,
+ additionalGridFields,
+ input2State,
+}: CreateContactsPageOptions) {
+ function BrevoTestContactsPage(): JSX.Element {
+ const intl = useIntl();
+ const { scope: completeScope } = useContentScope();
+
+ const scope = scopeParts.reduce((acc, scopePart) => {
+ acc[scopePart] = completeScope[scopePart];
+ return acc;
+ }, {} as { [key: string]: unknown });
+
+ return (
+
+
+
+ } />
+
+
+
+ {(selectedId) => (
+
+ )}
+
+
+
+
+
+
+ );
+ }
+
+ return BrevoTestContactsPage;
+}
+
+export { createBrevoTestContactsPage };
diff --git a/packages/admin/src/brevoTestContacts/form/BrevoTestContactForm.gql.ts b/packages/admin/src/brevoTestContacts/form/BrevoTestContactForm.gql.ts
new file mode 100644
index 00000000..525841f6
--- /dev/null
+++ b/packages/admin/src/brevoTestContacts/form/BrevoTestContactForm.gql.ts
@@ -0,0 +1,37 @@
+import { DocumentNode, gql } from "@apollo/client";
+
+export const brevoContactFormQuery = (brevoTestContactFormFragment: DocumentNode) => gql`
+ query BrevoContactForm($id: Int!, $scope: EmailCampaignContentScopeInput!) {
+ brevoContact(id: $id, scope: $scope) {
+ id
+ modifiedAt
+ ...BrevoTestContactForm
+ }
+ }
+ ${brevoTestContactFormFragment}
+`;
+
+export const brevoContactFormCheckForChangesQuery = gql`
+ query BrevoContactFormCheckForChanges($id: Int!, $scope: EmailCampaignContentScopeInput!) {
+ brevoContact(id: $id, scope: $scope) {
+ modifiedAt
+ }
+ }
+`;
+
+export const createBrevoTestContactMutation = gql`
+ mutation CreateBrevoTestContact($scope: EmailCampaignContentScopeInput!, $input: BrevoTestContactInput!) {
+ createBrevoTestContact(scope: $scope, input: $input)
+ }
+`;
+
+export const updateBrevoContactMutation = (brevoTestContactFormFragment: DocumentNode) => gql`
+ mutation UpdateBrevoContact($id: Int!, $input: BrevoContactUpdateInput!, $scope: EmailCampaignContentScopeInput!) {
+ updateBrevoContact(id: $id, input: $input, scope: $scope) {
+ id
+ modifiedAt
+ ...BrevoTestContactForm
+ }
+ }
+ ${brevoTestContactFormFragment}
+`;
diff --git a/packages/admin/src/brevoTestContacts/form/BrevoTestContactForm.tsx b/packages/admin/src/brevoTestContacts/form/BrevoTestContactForm.tsx
new file mode 100644
index 00000000..7c22a5ba
--- /dev/null
+++ b/packages/admin/src/brevoTestContacts/form/BrevoTestContactForm.tsx
@@ -0,0 +1,216 @@
+import { DocumentNode, gql, useApolloClient, useQuery } from "@apollo/client";
+import {
+ Alert,
+ FinalForm,
+ FinalFormSaveSplitButton,
+ FinalFormSubmitEvent,
+ FormSection,
+ Loading,
+ MainContent,
+ TextField,
+ Toolbar,
+ ToolbarActions,
+ ToolbarFillSpace,
+ ToolbarItem,
+ ToolbarTitleItem,
+ useFormApiRef,
+ useStackApi,
+} from "@comet/admin";
+import { ArrowLeft } from "@comet/admin-icons";
+import { ContentScopeInterface, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin";
+import { Card, IconButton } from "@mui/material";
+import { Box } from "@mui/system";
+import { FormApi } from "final-form";
+import React from "react";
+import { FormattedMessage } from "react-intl";
+
+import {
+ brevoContactFormCheckForChangesQuery,
+ brevoContactFormQuery,
+ createBrevoTestContactMutation,
+ updateBrevoContactMutation,
+} from "./BrevoTestContactForm.gql";
+import {
+ GQLBrevoContactFormCheckForChangesQuery,
+ GQLBrevoContactFormCheckForChangesQueryVariables,
+ GQLBrevoContactFormQuery,
+ GQLBrevoContactFormQueryVariables,
+ GQLCreateBrevoTestContactMutation,
+ GQLCreateBrevoTestContactMutationVariables,
+ GQLUpdateBrevoContactMutation,
+ GQLUpdateBrevoContactMutationVariables,
+} from "./BrevoTestContactForm.gql.generated";
+
+export type EditBrevoContactFormValues = {
+ [key: string]: unknown;
+};
+
+type EditBrevoContactFormValuesWithAttributes = EditBrevoContactFormValues & {
+ email: string;
+};
+
+interface FormProps {
+ id?: number;
+ scope: ContentScopeInterface;
+ additionalFormFields?: React.ReactNode;
+ additionalAttributesFragment?: { name: string; fragment: DocumentNode };
+ input2State?: (values?: EditBrevoContactFormValues) => EditBrevoContactFormValues;
+}
+
+export function BrevoTestContactForm({ id, scope, input2State, additionalFormFields, additionalAttributesFragment }: FormProps): React.ReactElement {
+ const stackApi = useStackApi();
+ const client = useApolloClient();
+ const mode = id ? "edit" : "add";
+ const formApiRef = useFormApiRef();
+
+ const brevoTestContactFormFragment = gql`
+ fragment BrevoTestContactForm on BrevoContact {
+ email
+ createdAt
+ emailBlacklisted
+ smsBlacklisted
+ ${additionalAttributesFragment ? "...".concat(additionalAttributesFragment?.name) : ""}
+ }
+ ${additionalAttributesFragment?.fragment ?? ""}
+`;
+ const { data, error, loading, refetch } = useQuery(
+ brevoContactFormQuery(brevoTestContactFormFragment),
+ id ? { variables: { id, scope } } : { skip: true },
+ );
+
+ const initialValues = React.useMemo>(() => {
+ let baseInitialValues = {
+ email: "",
+ };
+
+ if (input2State) {
+ baseInitialValues = {
+ ...baseInitialValues,
+ ...input2State(data?.brevoContact),
+ };
+ }
+
+ return data?.brevoContact?.email ? { ...baseInitialValues, email: data.brevoContact.email } : baseInitialValues;
+ }, [data?.brevoContact, input2State]);
+
+ const saveConflict = useFormSaveConflict({
+ checkConflict: async () => {
+ if (!id) {
+ return false;
+ }
+ const { data: updatedData } = await client.query<
+ GQLBrevoContactFormCheckForChangesQuery,
+ GQLBrevoContactFormCheckForChangesQueryVariables
+ >({
+ query: brevoContactFormCheckForChangesQuery,
+ variables: { id, scope },
+ fetchPolicy: "no-cache",
+ });
+
+ return resolveHasSaveConflict(data?.brevoContact?.modifiedAt, updatedData.brevoContact.modifiedAt);
+ },
+ formApiRef,
+ loadLatestVersion: async () => {
+ await refetch();
+ },
+ });
+
+ const handleSubmit = async (
+ state: EditBrevoContactFormValuesWithAttributes,
+ form: FormApi,
+ event: FinalFormSubmitEvent,
+ ) => {
+ if (await saveConflict.checkForConflicts()) {
+ throw new Error("Conflicts detected");
+ }
+
+ const output = {
+ ...state,
+ blocked: false,
+ };
+
+ if (mode === "edit") {
+ if (!id) {
+ throw new Error("Missing id in edit mode");
+ }
+ const { email, ...rest } = output;
+ await client.mutate({
+ mutation: updateBrevoContactMutation(brevoTestContactFormFragment),
+ variables: { id, input: rest, scope },
+ });
+ } else {
+ const { data: mutationResponse } = await client.mutate({
+ mutation: createBrevoTestContactMutation,
+ variables: { scope, input: output },
+ });
+ if (!event.navigatingBack) {
+ const response = mutationResponse?.createBrevoTestContact;
+
+ if (response === "SUCCESSFUL") {
+ setTimeout(() => {
+ stackApi?.goBack();
+ });
+ } else if (response === "ERROR_CONTAINED_IN_ECG_RTR_LIST") {
+ throw new Error("Contact contained in ECG RTR list, cannot create contact");
+ } else {
+ throw new Error("Error creating contact");
+ }
+ }
+ }
+ };
+
+ if (error) throw error;
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ apiRef={formApiRef} onSubmit={handleSubmit} mode={mode} initialValues={initialValues}>
+ <>
+ {saveConflict.dialogs}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {mode === "edit" && (
+
+
+
+
+
+ )}
+ }
+ disabled={mode === "edit"}
+ />
+
+ {additionalFormFields && (
+
+ }>
+ {additionalFormFields}
+
+
+ )}
+
+ >
+
+ );
+}
diff --git a/packages/admin/src/index.ts b/packages/admin/src/index.ts
index 74d8f1b9..d69bcaac 100644
--- a/packages/admin/src/index.ts
+++ b/packages/admin/src/index.ts
@@ -1,6 +1,7 @@
export { BrevoConfigPage } from "./brevoConfiguration/BrevoConfigPage";
export { createBrevoContactsPage } from "./brevoContacts/BrevoContactsPage";
export { EditBrevoContactFormValues } from "./brevoContacts/form/BrevoContactForm";
+export { createBrevoTestContactsPage } from "./brevoTestContacts/BrevoTestContactsPage";
export { BrevoConfig, BrevoConfigProvider, useBrevoConfig } from "./common/BrevoConfigProvider";
export { createEmailCampaignsPage } from "./emailCampaigns/EmailCampaignsPage";
export { EditTargetGroupFinalFormValues } from "./targetGroups/TargetGroupForm";
diff --git a/packages/api/generate-schema.ts b/packages/api/generate-schema.ts
index ccbb39d9..a005efd0 100644
--- a/packages/api/generate-schema.ts
+++ b/packages/api/generate-schema.ts
@@ -12,6 +12,7 @@ import { createBrevoContactResolver } from "./src/brevo-contact/brevo-contact.re
import { createBrevoContactImportResolver } from "./src/brevo-contact/brevo-contact-import.resolver";
import { BrevoContactFactory } from "./src/brevo-contact/dto/brevo-contact.factory";
import { BrevoContactInputFactory } from "./src/brevo-contact/dto/brevo-contact-input.factory";
+import { BrevoTestContactInputFactory } from "./src/brevo-contact/dto/brevo-test-contact-input.factory";
import { SubscribeInputFactory } from "./src/brevo-contact/dto/subscribe-input.factory";
import { EmailCampaignInputFactory } from "./src/email-campaign/dto/email-campaign-input.factory";
import { createEmailCampaignsResolver } from "./src/email-campaign/email-campaign.resolver";
@@ -64,6 +65,8 @@ async function generateSchema(): Promise {
const BrevoContact = BrevoContactFactory.create({});
const [BrevoContactInput, BrevoContactUpdateInput] = BrevoContactInputFactory.create({ Scope: EmailCampaignScope });
+ const [BrevoTestContactInput, BrevoTestContactUpdateInput] = BrevoTestContactInputFactory.create({ Scope: EmailCampaignScope });
+
const BrevoContactSubscribeInput = SubscribeInputFactory.create({ Scope: EmailCampaignScope });
const BrevoContactResolver = createBrevoContactResolver({
BrevoContact,
@@ -71,6 +74,8 @@ async function generateSchema(): Promise {
Scope: EmailCampaignScope,
BrevoContactInput,
BrevoContactUpdateInput,
+ BrevoTestContactInput,
+ BrevoTestContactUpdateInput,
});
const BrevoContactImportResolver = createBrevoContactImportResolver({
BrevoContact,
diff --git a/packages/api/schema.gql b/packages/api/schema.gql
index d4627760..0c8f7b55 100644
--- a/packages/api/schema.gql
+++ b/packages/api/schema.gql
@@ -131,6 +131,7 @@ type TargetGroup implements DocumentInterface {
createdAt: DateTime!
title: String!
isMainList: Boolean!
+ isTestList: Boolean
brevoId: Int!
totalSubscribers: Int!
scope: EmailCampaignContentScope!
@@ -196,6 +197,7 @@ input EmailCampaignContentScopeInput {
type Query {
brevoContact(id: Int!, scope: EmailCampaignContentScopeInput!): BrevoContact!
brevoContacts(targetGroupId: ID, email: String, scope: EmailCampaignContentScopeInput!, offset: Int! = 0, limit: Int! = 25): PaginatedBrevoContacts!
+ brevoTestContacts(targetGroupId: ID, email: String, scope: EmailCampaignContentScopeInput!, offset: Int! = 0, limit: Int! = 25): PaginatedBrevoContacts!
manuallyAssignedBrevoContacts(offset: Int! = 0, limit: Int! = 25, targetGroupId: ID!, email: String): PaginatedBrevoContacts!
targetGroup(id: ID!): TargetGroup!
targetGroups(scope: EmailCampaignContentScopeInput!, search: String, filter: TargetGroupFilter, sort: [TargetGroupSort!], offset: Int! = 0, limit: Int! = 25): PaginatedTargetGroups!
@@ -210,6 +212,7 @@ input TargetGroupFilter {
createdAt: DateTimeFilter
updatedAt: DateTimeFilter
title: StringFilter
+ isTestList: BooleanFilter
and: [TargetGroupFilter!]
or: [TargetGroupFilter!]
}
@@ -231,6 +234,10 @@ input StringFilter {
notEqual: String
}
+input BooleanFilter {
+ equal: Boolean
+}
+
input TargetGroupSort {
field: TargetGroupSortField!
direction: SortDirection! = ASC
@@ -273,7 +280,9 @@ enum EmailCampaignSortField {
type Mutation {
updateBrevoContact(id: Int!, scope: EmailCampaignContentScopeInput!, input: BrevoContactUpdateInput!): BrevoContact!
createBrevoContact(scope: EmailCampaignContentScopeInput!, input: BrevoContactInput!): SubscribeResponse!
+ createBrevoTestContact(scope: EmailCampaignContentScopeInput!, input: BrevoTestContactInput!): SubscribeResponse!
deleteBrevoContact(id: Int!, scope: EmailCampaignContentScopeInput!): Boolean!
+ deleteBrevoTestContact(id: Int!, scope: EmailCampaignContentScopeInput!): Boolean!
subscribeBrevoContact(input: SubscribeInput!, scope: EmailCampaignContentScopeInput!): SubscribeResponse!
createTargetGroup(scope: EmailCampaignContentScopeInput!, input: TargetGroupInput!): TargetGroup!
addBrevoContactsToTargetGroup(id: ID!, input: AddBrevoContactsInput!): Boolean!
@@ -306,6 +315,11 @@ input BrevoContactInput {
redirectionUrl: String!
}
+input BrevoTestContactInput {
+ email: String!
+ blocked: Boolean!
+}
+
input SubscribeInput {
email: String!
redirectionUrl: String!
diff --git a/packages/api/src/brevo-api/brevo-api-contact.service.ts b/packages/api/src/brevo-api/brevo-api-contact.service.ts
index 3627751d..cbed193f 100644
--- a/packages/api/src/brevo-api/brevo-api-contact.service.ts
+++ b/packages/api/src/brevo-api/brevo-api-contact.service.ts
@@ -62,6 +62,21 @@ export class BrevoApiContactsService {
}
}
+ public async createTestContact(
+ { email, attributes }: Brevo.CreateContact,
+ brevoIds: number[],
+ scope: EmailCampaignScopeInterface,
+ ): Promise {
+ const contact = {
+ email,
+ listIds: brevoIds,
+ attributes,
+ };
+ const { response } = await this.getContactsApi(scope).createContact(contact);
+
+ return response.statusCode === 204 || response.statusCode === 201;
+ }
+
public async updateContact(
id: number,
{
diff --git a/packages/api/src/brevo-contact/brevo-contact.module.ts b/packages/api/src/brevo-contact/brevo-contact.module.ts
index bd3f6e7a..e59d379f 100644
--- a/packages/api/src/brevo-contact/brevo-contact.module.ts
+++ b/packages/api/src/brevo-contact/brevo-contact.module.ts
@@ -14,6 +14,7 @@ import { createBrevoContactImportResolver } from "./brevo-contact-import.resolve
import { BrevoContactsService } from "./brevo-contacts.service";
import { BrevoContactFactory } from "./dto/brevo-contact.factory";
import { BrevoContactInputFactory } from "./dto/brevo-contact-input.factory";
+import { BrevoTestContactInputFactory } from "./dto/brevo-test-contact-input.factory";
import { SubscribeInputFactory } from "./dto/subscribe-input.factory";
import { EcgRtrListService } from "./ecg-rtr-list/ecg-rtr-list.service";
import { IsValidRedirectURLConstraint } from "./validator/redirect-url.validator";
@@ -30,12 +31,15 @@ export class BrevoContactModule {
const BrevoContact = BrevoContactFactory.create({ BrevoContactAttributes });
const BrevoContactSubscribeInput = SubscribeInputFactory.create({ BrevoContactAttributes, Scope });
const [BrevoContactInput, BrevoContactUpdateInput] = BrevoContactInputFactory.create({ BrevoContactAttributes, Scope });
+ const [BrevoTestContactInput] = BrevoTestContactInputFactory.create({ BrevoContactAttributes, Scope });
+
const BrevoContactResolver = createBrevoContactResolver({
BrevoContact,
BrevoContactSubscribeInput,
Scope,
BrevoContactInput,
BrevoContactUpdateInput,
+ BrevoTestContactInput,
});
const BrevoContactImportResolver = createBrevoContactImportResolver({ Scope, BrevoContact });
diff --git a/packages/api/src/brevo-contact/brevo-contact.resolver.ts b/packages/api/src/brevo-contact/brevo-contact.resolver.ts
index a0df062c..02c5a07c 100644
--- a/packages/api/src/brevo-contact/brevo-contact.resolver.ts
+++ b/packages/api/src/brevo-contact/brevo-contact.resolver.ts
@@ -15,6 +15,7 @@ import { BrevoContactsService } from "./brevo-contacts.service";
import { BrevoContactInterface } from "./dto/brevo-contact.factory";
import { BrevoContactInputInterface, BrevoContactUpdateInputInterface } from "./dto/brevo-contact-input.factory";
import { BrevoContactsArgsFactory } from "./dto/brevo-contacts.args";
+import { BrevoTestContactInputInterface } from "./dto/brevo-test-contact-input.factory";
import { ManuallyAssignedBrevoContactsArgs } from "./dto/manually-assigned-brevo-contacts.args";
import { SubscribeInputInterface } from "./dto/subscribe-input.factory";
import { SubscribeResponse } from "./dto/subscribe-response.enum";
@@ -26,11 +27,13 @@ export function createBrevoContactResolver({
Scope,
BrevoContactInput,
BrevoContactUpdateInput,
+ BrevoTestContactInput,
}: {
BrevoContact: Type;
BrevoContactSubscribeInput: Type;
BrevoContactInput: Type;
BrevoContactUpdateInput: Type>;
+ BrevoTestContactInput: Type;
Scope: Type;
}): Type {
@ObjectType()
@@ -98,6 +101,29 @@ export function createBrevoContactResolver({
return new PaginatedBrevoContacts(contacts, count, { offset, limit });
}
+ @Query(() => PaginatedBrevoContacts)
+ async brevoTestContacts(@Args() { offset, limit, email, scope }: BrevoContactsArgs): Promise {
+ const where: FilterQuery = { scope, isMainList: false, isTestList: true };
+
+ let targetGroup = await this.targetGroupRepository.findOne(where);
+
+ if (!targetGroup) {
+ // when there is no test target group for the scope, create one
+ targetGroup = await this.targetGroupService.createIfNotExistTestTargetGroupForScope(scope);
+ }
+
+ if (email) {
+ const contact = await this.brevoContactsApiService.getContactInfoByEmail(email, scope);
+ if (contact && contact.listIds.includes(targetGroup.brevoId)) {
+ return new PaginatedBrevoContacts([contact], 1, { offset, limit });
+ }
+ return new PaginatedBrevoContacts([], 0, { offset, limit });
+ }
+ const [contacts, count] = await this.brevoContactsApiService.findContactsByListId(targetGroup.brevoId, limit, offset, targetGroup.scope);
+
+ return new PaginatedBrevoContacts(contacts, count, { offset, limit });
+ }
+
@Query(() => PaginatedBrevoContacts)
async manuallyAssignedBrevoContacts(
@Args() { offset, limit, email, targetGroupId }: ManuallyAssignedBrevoContactsArgs,
@@ -146,12 +172,22 @@ export function createBrevoContactResolver({
const mainListIds = (await this.targetGroupRepository.find({ brevoId: { $in: assignedListIds }, isMainList: true })).map(
(targetGroup) => targetGroup.brevoId,
);
-
const updatedNonMainListIds = await this.brevoContactsService.getTargetGroupIdsForExistingContact({
contact,
});
+ const testTargetGroup = await this.targetGroupRepository.findOne({ scope, isMainList: false, isTestList: true });
+ const contactIncludesTestList = testTargetGroup?.brevoId ? contact.listIds.includes(testTargetGroup.brevoId) : false;
+
+ if (testTargetGroup && contactIncludesTestList) {
+ const testListId = testTargetGroup.brevoId;
+ if (!updatedNonMainListIds.includes(testListId)) {
+ updatedNonMainListIds.push(testListId);
+ }
+ }
+
// update contact again with updated list ids depending on new attributes
+
const contactWithUpdatedLists = await this.brevoContactsApiService.updateContact(
id,
{
@@ -190,13 +226,108 @@ export function createBrevoContactResolver({
return SubscribeResponse.ERROR_UNKNOWN;
}
+ @Mutation(() => SubscribeResponse)
+ @RequiredPermission(["brevo-newsletter-test-contacts"], { skipScopeCheck: true })
+ async createBrevoTestContact(
+ @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope,
+ @Args("input", { type: () => BrevoTestContactInput })
+ input: BrevoContactInputInterface,
+ ): Promise {
+ const where: FilterQuery = { scope, isMainList: false, isTestList: true };
+ const targetGroup = await this.targetGroupRepository.findOne(where);
+ const contact = await this.brevoContactsApiService.getContactInfoByEmail(input.email, scope);
+
+ if (contact && targetGroup) {
+ const listIds: number[] = contact.listIds ? [...contact.listIds] : [];
+ listIds.push(targetGroup.brevoId);
+
+ await this.brevoContactsApiService.updateContact(
+ contact.id,
+ {
+ listIds,
+ },
+ scope,
+ );
+
+ return SubscribeResponse.SUCCESSFUL;
+ } else {
+ const created = await this.brevoContactsService.createTestContact({
+ email: input.email,
+ attributes: input.attributes,
+ scope,
+ });
+
+ if (created) {
+ return SubscribeResponse.SUCCESSFUL;
+ }
+
+ return SubscribeResponse.ERROR_UNKNOWN;
+ }
+ }
+
@Mutation(() => Boolean)
@AffectedEntity(BrevoContact)
async deleteBrevoContact(
@Args("id", { type: () => Int }) id: number,
@Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope,
): Promise {
- return this.brevoContactsApiService.deleteContact(id, scope);
+ const contact = await this.brevoContactsApiService.findContact(id, scope);
+ if (!contact) return false;
+
+ const where: FilterQuery = { scope, isMainList: false, isTestList: true };
+ const testTargetGroup = await this.targetGroupRepository.findOne(where);
+ const contactIncludesTestList = testTargetGroup?.brevoId ? contact.listIds.includes(testTargetGroup.brevoId) : false;
+
+ if (testTargetGroup && contactIncludesTestList) {
+ const testListId = testTargetGroup.brevoId;
+
+ const unlinkListIds = contact.listIds.filter((id) => id !== testListId);
+
+ await this.brevoContactsApiService.updateContact(
+ contact.id,
+ {
+ listIds: [testListId],
+ unlinkListIds,
+ },
+ scope,
+ );
+ return true;
+ } else {
+ return this.brevoContactsApiService.deleteContact(id, scope);
+ }
+ }
+
+ @Mutation(() => Boolean)
+ @AffectedEntity(BrevoContact)
+ async deleteBrevoTestContact(
+ @Args("id", { type: () => Int }) id: number,
+ @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope,
+ ): Promise {
+ const contact = await this.brevoContactsApiService.findContact(id, scope);
+ if (!contact) return false;
+
+ const where: FilterQuery = { scope, isMainList: false, isTestList: true };
+ const testTargetGroup = await this.targetGroupRepository.findOne(where);
+ const mainTargetGroup = await this.targetGroupRepository.findOne({ scope, isMainList: true });
+ const mainListIncludesContact = mainTargetGroup?.brevoId ? contact.listIds.includes(mainTargetGroup.brevoId) : false;
+
+ if (testTargetGroup && mainListIncludesContact) {
+ const testListId = testTargetGroup.brevoId;
+
+ const linkListIds = contact.listIds.filter((id) => id !== testListId);
+
+ await this.brevoContactsApiService.updateContact(
+ contact.id,
+ {
+ listIds: linkListIds,
+ unlinkListIds: [testListId],
+ },
+ scope,
+ );
+ return true;
+ } else {
+ return this.brevoContactsApiService.deleteContact(id, scope);
+ }
}
@Mutation(() => SubscribeResponse)
diff --git a/packages/api/src/brevo-contact/brevo-contacts.service.ts b/packages/api/src/brevo-contact/brevo-contacts.service.ts
index ac77a491..298b70c5 100644
--- a/packages/api/src/brevo-contact/brevo-contacts.service.ts
+++ b/packages/api/src/brevo-contact/brevo-contacts.service.ts
@@ -53,6 +53,21 @@ export class BrevoContactsService {
return created;
}
+ public async createTestContact({
+ email,
+ attributes,
+ scope,
+ }: {
+ email: string;
+ attributes?: BrevoContactAttributesInterface;
+ scope: EmailCampaignScopeInterface;
+ }): Promise {
+ const testTargetGroupForScope = await this.targetGroupService.createIfNotExistTestTargetGroupForScope(scope);
+
+ const created = await this.brevoContactsApiService.createTestContact({ email, attributes }, [testTargetGroupForScope.brevoId], scope);
+ return created;
+ }
+
public async getTargetGroupIdsForNewContact({
contactAttributes,
scope,
@@ -72,7 +87,10 @@ export class BrevoContactsService {
offset += targetGroups.length;
for (const targetGroup of targetGroups) {
- const contactIsInTargetGroup = this.targetGroupService.checkIfContactIsInTargetGroup(contactAttributes, targetGroup.filters);
+ const contactIsInTargetGroup = this.targetGroupService.checkIfContactIsInTargetGroupByAttributes(
+ contactAttributes,
+ targetGroup.filters,
+ );
if (contactIsInTargetGroup) {
targetGroupIds.push(targetGroup.brevoId);
@@ -114,7 +132,7 @@ export class BrevoContactsService {
offset += targetGroups.length;
for (const targetGroup of targetGroups) {
- const contactIsInTargetGroupByAttributes = this.targetGroupService.checkIfContactIsInTargetGroup(
+ const contactIsInTargetGroupByAttributes = this.targetGroupService.checkIfContactIsInTargetGroupByAttributes(
contact?.attributes,
targetGroup.filters,
);
diff --git a/packages/api/src/brevo-contact/dto/brevo-test-contact-input.factory.ts b/packages/api/src/brevo-contact/dto/brevo-test-contact-input.factory.ts
new file mode 100644
index 00000000..47a283d8
--- /dev/null
+++ b/packages/api/src/brevo-contact/dto/brevo-test-contact-input.factory.ts
@@ -0,0 +1,61 @@
+import { IsUndefinable } from "@comet/cms-api";
+import { Type } from "@nestjs/common";
+import { Field, InputType } from "@nestjs/graphql";
+import { Type as TypeTransformer } from "class-transformer";
+import { IsBoolean, IsNotEmpty, IsString, ValidateNested } from "class-validator";
+
+import { BrevoContactAttributesInterface, EmailCampaignScopeInterface } from "../../types";
+
+export interface BrevoTestContactInputInterface {
+ email: string;
+ blocked?: boolean;
+ attributes?: BrevoContactAttributesInterface;
+}
+
+export interface BrevoTestContactUpdateInputInterface {
+ blocked?: boolean;
+ attributes?: BrevoContactAttributesInterface;
+}
+
+export class BrevoTestContactInputFactory {
+ static create({
+ BrevoContactAttributes,
+ Scope,
+ }: {
+ BrevoContactAttributes?: Type;
+ Scope: Type;
+ }): [Type] {
+ @InputType({
+ isAbstract: true,
+ })
+ class BrevoTestContactInputBase implements BrevoTestContactInputInterface {
+ @IsNotEmpty()
+ @IsString()
+ @Field()
+ email: string;
+
+ @IsBoolean()
+ @Field()
+ @IsUndefinable()
+ blocked?: boolean;
+ }
+
+ if (BrevoContactAttributes) {
+ @InputType()
+ class BrevoTestContactInput extends BrevoTestContactInputBase {
+ @Field(() => BrevoContactAttributes, { nullable: true })
+ @TypeTransformer(() => BrevoContactAttributes)
+ @ValidateNested()
+ @IsUndefinable()
+ attributes?: BrevoContactAttributesInterface;
+ }
+
+ return [BrevoTestContactInput];
+ }
+
+ @InputType()
+ class BrevoTestContactInput extends BrevoTestContactInputBase {}
+
+ return [BrevoTestContactInput];
+ }
+}
diff --git a/packages/api/src/mikro-orm/migrations/Migration20240830112400.ts b/packages/api/src/mikro-orm/migrations/Migration20240830112400.ts
new file mode 100644
index 00000000..baeac849
--- /dev/null
+++ b/packages/api/src/mikro-orm/migrations/Migration20240830112400.ts
@@ -0,0 +1,9 @@
+import { Migration } from "@mikro-orm/migrations";
+
+export class Migration20240830112400 extends Migration {
+ async up(): Promise {
+ this.addSql('alter table "TargetGroup" add column "isTestList" boolean not null default false;');
+
+ this.addSql('alter table "TargetGroup" alter column "isTestList" drop default;');
+ }
+}
diff --git a/packages/api/src/mikro-orm/migrations/migrations.ts b/packages/api/src/mikro-orm/migrations/migrations.ts
index 688bdfe3..48b2aab4 100644
--- a/packages/api/src/mikro-orm/migrations/migrations.ts
+++ b/packages/api/src/mikro-orm/migrations/migrations.ts
@@ -8,6 +8,7 @@ import { Migration20240619092554 } from "./Migration20240619092554";
import { Migration20240619145217 } from "./Migration20240619145217";
import { Migration20240621102349 } from "./Migration20240621102349";
import { Migration20240819214939 } from "./Migration20240819214939";
+import { Migration20240830112400 } from "./Migration20240830112400";
import { Migration20241016123307 } from "./Migration20241016123307";
export const migrationsList: MigrationObject[] = [
@@ -19,5 +20,6 @@ export const migrationsList: MigrationObject[] = [
{ name: "Migration20240619145217", class: Migration20240619145217 },
{ name: "Migration20240621102349", class: Migration20240621102349 },
{ name: "Migration20240819214939", class: Migration20240819214939 },
+ { name: "Migration20240830112400", class: Migration20240830112400 },
{ name: "Migration20241016123307", class: Migration20241016123307 },
];
diff --git a/packages/api/src/target-group/dto/target-group.filter.ts b/packages/api/src/target-group/dto/target-group.filter.ts
index f25832df..ce2fad55 100644
--- a/packages/api/src/target-group/dto/target-group.filter.ts
+++ b/packages/api/src/target-group/dto/target-group.filter.ts
@@ -1,4 +1,4 @@
-import { DateTimeFilter, StringFilter } from "@comet/cms-api";
+import { BooleanFilter, DateTimeFilter, StringFilter } from "@comet/cms-api";
import { Field, InputType } from "@nestjs/graphql";
import { Type } from "class-transformer";
import { IsOptional, ValidateNested } from "class-validator";
@@ -23,6 +23,12 @@ export class TargetGroupFilter {
@Type(() => StringFilter)
title?: StringFilter;
+ @Field(() => BooleanFilter, { nullable: true })
+ @ValidateNested()
+ @IsOptional()
+ @Type(() => BooleanFilter)
+ isTestList?: BooleanFilter;
+
@Field(() => [TargetGroupFilter], { nullable: true })
@Type(() => TargetGroupFilter)
@ValidateNested({ each: true })
diff --git a/packages/api/src/target-group/entity/target-group-entity.factory.ts b/packages/api/src/target-group/entity/target-group-entity.factory.ts
index 533f60d7..f5c4a814 100644
--- a/packages/api/src/target-group/entity/target-group-entity.factory.ts
+++ b/packages/api/src/target-group/entity/target-group-entity.factory.ts
@@ -20,6 +20,7 @@ export interface TargetGroupInterface {
filters?: BrevoContactFilterAttributesInterface;
assignedContactsTargetGroupBrevoId?: number;
campaigns: Collection;
+ isTestList: boolean;
}
export class TargetGroupEntityFactory {
@@ -58,6 +59,10 @@ export class TargetGroupEntityFactory {
@Field()
isMainList: boolean;
+ @Property({ columnType: "boolean" })
+ @Field()
+ isTestList: boolean;
+
@Property({ columnType: "int" })
@Field(() => Int)
brevoId: number;
diff --git a/packages/api/src/target-group/target-group.resolver.ts b/packages/api/src/target-group/target-group.resolver.ts
index d8965457..a34ba874 100644
--- a/packages/api/src/target-group/target-group.resolver.ts
+++ b/packages/api/src/target-group/target-group.resolver.ts
@@ -52,6 +52,7 @@ export function createTargetGroupsResolver({
async targetGroups(@Args() { search, filter, sort, offset, limit, scope }: TargetGroupsArgs): Promise {
const where = this.targetGroupsService.getFindCondition({ search, filter });
where.scope = scope;
+ where.isTestList = false;
const options: FindOptions = { offset, limit };
@@ -90,7 +91,7 @@ export function createTargetGroupsResolver({
const brevoId = await this.brevoApiContactsService.createBrevoContactList(input.title, scope);
if (brevoId) {
- const targetGroup = this.repository.create({ ...input, brevoId, scope, isMainList: false });
+ const targetGroup = this.repository.create({ ...input, brevoId, scope, isMainList: false, isTestList: false });
await this.entityManager.flush();
@@ -142,7 +143,7 @@ export function createTargetGroupsResolver({
throw new Error(`Brevo contact with id ${input.brevoContactId} not found`);
}
- const contactIsInTargetGroupByAttributes = this.targetGroupsService.checkIfContactIsInTargetGroup(
+ const contactIsInTargetGroupByAttributes = this.targetGroupsService.checkIfContactIsInTargetGroupByAttributes(
brevoContact.attributes,
targetGroup.filters,
);
diff --git a/packages/api/src/target-group/target-groups.service.ts b/packages/api/src/target-group/target-groups.service.ts
index 096326aa..075c98b2 100644
--- a/packages/api/src/target-group/target-groups.service.ts
+++ b/packages/api/src/target-group/target-groups.service.ts
@@ -2,6 +2,7 @@ import { filtersToMikroOrmQuery, searchToMikroOrmQuery } from "@comet/cms-api";
import { EntityManager, EntityRepository, FilterQuery, ObjectQuery, wrap } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { Injectable } from "@nestjs/common";
+import { stringify } from "querystring";
import { handleBrevoError } from "../brevo-api/brevo-api.utils";
import { BrevoApiContactsService } from "../brevo-api/brevo-api-contact.service";
@@ -32,7 +33,7 @@ export class TargetGroupsService {
return andFilters.length > 0 ? { $and: andFilters } : {};
}
- public checkIfContactIsInTargetGroup(
+ public checkIfContactIsInTargetGroupByAttributes(
contactAttributes?: BrevoContactAttributesInterface,
filters?: BrevoContactFilterAttributesInterface,
): boolean {
@@ -87,8 +88,7 @@ export class TargetGroupsService {
const contactsNotInContactList: BrevoContactInterface[] = [];
for (const contact of contacts) {
- const contactIsInTargetGroupByFilters = this.checkIfContactIsInTargetGroup(contact.attributes, filters);
-
+ const contactIsInTargetGroupByFilters = this.checkIfContactIsInTargetGroupByAttributes(contact.attributes, filters);
const manuallyAssignedTargetGroup = targetGroup.assignedContactsTargetGroupBrevoId;
const contactIsManuallyAssignedToTargetGroup = manuallyAssignedTargetGroup
? contact.listIds.includes(manuallyAssignedTargetGroup)
@@ -150,7 +150,7 @@ export class TargetGroupsService {
const brevoId = await this.brevoApiContactsService.createBrevoContactList(title, scope);
if (brevoId) {
- const mainTargetGroupForScope = this.repository.create({ title, brevoId, scope, isMainList: true });
+ const mainTargetGroupForScope = this.repository.create({ title, brevoId, scope, isMainList: true, isTestList: false });
await this.entityManager.flush();
@@ -164,6 +164,28 @@ export class TargetGroupsService {
}
}
+ public async createIfNotExistTestTargetGroupForScope(scope: EmailCampaignScopeInterface): Promise {
+ const testList = await this.repository.findOne({ scope, isMainList: false, isTestList: true });
+
+ if (testList) {
+ return testList;
+ }
+
+ const title = `Test list for scope: ${stringify(scope)}`;
+
+ const brevoId = await this.brevoApiContactsService.createBrevoContactList(title, scope);
+
+ if (brevoId) {
+ const testTargetGroupForScope = this.repository.create({ title, brevoId, scope, isMainList: false, isTestList: true });
+
+ await this.entityManager.flush();
+
+ return testTargetGroupForScope;
+ }
+
+ throw new Error("Brevo Error: Could not create contact list");
+ }
+
public async createIfNotExistsManuallyAssignedContactsTargetGroup(targetGroup: TargetGroupInterface) {
try {
if (targetGroup.assignedContactsTargetGroupBrevoId) {
diff --git a/renovate.json b/renovate.json
index 75688cb3..7aba89d1 100644
--- a/renovate.json
+++ b/renovate.json
@@ -1,6 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "extends": ["config:recommended"],
+ "extends": ["config:recommended", ":semanticCommitsDisabled"],
"baseBranches": ["next"],
"packageRules": [
{
@@ -9,10 +9,6 @@
"matchPackageNames": ["/@comet/*/"],
"labels": ["comet", "dependencies"]
},
- {
- "groupName": "types",
- "matchPackageNames": ["/@types/*/"]
- },
{
"groupName": "linters",
"matchPackageNames": ["/^eslint/", "/^prettier/"]
@@ -68,5 +64,6 @@
}
],
"rangeStrategy": "bump",
+ "lockFileMaintenance": { "enabled": true },
"labels": ["dependencies"]
}