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" ?
{document.content && }
: 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 ( + + + + + + + + + + + + + ); +} + +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"] }