Skip to content

Commit

Permalink
Import contacts from CSV (#71)
Browse files Browse the repository at this point in the history
Implement CSV Brevo Contact import in Admin
Implement console command to upload CSV Brevo Contacts
  • Loading branch information
thomashuettmaier authored Aug 20, 2024
1 parent 3407537 commit aae0de4
Show file tree
Hide file tree
Showing 27 changed files with 989 additions and 140 deletions.
22 changes: 22 additions & 0 deletions .changeset/kind-penguins-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@comet/brevo-api": major
---

Add `redirectUrlForImport` to `BrevoModule` config

You must now pass a `redirectUrlForImport` to your `BrevoModule` config:

```ts
BrevoModule.register({
brevo: {
resolveConfig: (scope: EmailCampaignContentScope) => {
return {
// ...
redirectUrlForImport: config.brevo.redirectUrlForImport,
};
},
},
});
```

The `redirectUrlForImport` will usually be the site URL of a scope. It is used by the CSV contact import as redirect target after the user completes the double opt-in.
19 changes: 19 additions & 0 deletions .changeset/sharp-trees-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@comet/brevo-api": minor
"@comet/brevo-admin": minor
---

Add functionality to import Brevo contacts from CSV files

You can import CSV files via the Admin interface or via CLI command.

**Note:** For the import to work, you must provide a `redirectUrlForImport` to the `BrevoModule` in the API and an `apiUrl` to the `BrevoConfigProvider` in the admin. See the respective changelog entries for more information.

CLI command:

```bash
npm run --prefix api console import-brevo-contacts -- -p <path-to-csv-file> -s '<scope-json>' [--targetGroupIds <ids...>]

// Example:
npm run --prefix api console import-brevo-contacts -- -p test_contacts_import.csv -s '{"domain": "main", "language":"de"}' --targetGroupIds 2618c982-fdf8-4cab-9811-a21d3272c62c,c5197539-2529-48a7-9bd1-764e9620cbd2
```
13 changes: 13 additions & 0 deletions .changeset/violet-bees-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@comet/brevo-admin": major
---

Add `BrevoConfigProvider`

You must add the new `BrevoConfigProvider` in you `App.tsx`. The config requires passing the `apiUrl`:

```tsx
<BrevoConfigProvider value={{ apiUrl: config.apiUrl }}>{/* ... */}</BrevoConfigProvider>
```

The `apiUrl` is used by the CSV contact import to upload files to the API.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,6 @@ NEXT_PUBLIC_CAMPAIGN_IS_PREVIEW=false


# BREVO
REDIRECT_URL_FOR_IMPORT=$SITE_URL
BREVO_ALLOWED_REDIRECT_URL=http://${DEV_DOMAIN:-localhost}${WORKAROUND_DOTENV_ISSUE}:${SITE_PORT}
CAMPAIGNS_FRONTEND_URL=http://${DEV_DOMAIN:-localhost}${WORKAROUND_DOTENV_ISSUE}:${SITE_PORT} # doesn't work, TODO: add actual mailing frontend
37 changes: 20 additions & 17 deletions demo/admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "material-design-icons/iconfont/material-icons.css";

import { ApolloProvider } from "@apollo/client";
import { ErrorDialogHandler, MuiThemeProvider, RouterBrowserRouter, SnackbarProvider } from "@comet/admin";
import { BrevoConfigProvider } from "@comet/brevo-admin";
import {
AllCategories,
BuildInformationProvider,
Expand Down Expand Up @@ -77,23 +78,25 @@ export function App() {
<MuiThemeProvider theme={theme}>
<DndProvider backend={HTML5Backend}>
<SnackbarProvider>
<CmsBlockContextProvider
damConfig={{
apiUrl: config.apiUrl,
apiClient,
maxFileSize: config.dam.uploadsMaxFileSize,
maxSrcResolution: config.imgproxy.maxSrcResolution,
allowedImageAspectRatios: config.dam.allowedImageAspectRatios,
}}
pageTreeCategories={categories}
pageTreeDocumentTypes={pageTreeDocumentTypes}
>
<RouterBrowserRouter>
<GlobalStyle />
<Routes />
<ErrorDialogHandler />
</RouterBrowserRouter>
</CmsBlockContextProvider>
<BrevoConfigProvider value={{ apiUrl: config.apiUrl }}>
<CmsBlockContextProvider
damConfig={{
apiUrl: config.apiUrl,
apiClient,
maxFileSize: config.dam.uploadsMaxFileSize,
maxSrcResolution: config.imgproxy.maxSrcResolution,
allowedImageAspectRatios: config.dam.allowedImageAspectRatios,
}}
pageTreeCategories={categories}
pageTreeDocumentTypes={pageTreeDocumentTypes}
>
<RouterBrowserRouter>
<GlobalStyle />
<Routes />
<ErrorDialogHandler />
</RouterBrowserRouter>
</CmsBlockContextProvider>
</BrevoConfigProvider>
</SnackbarProvider>
</DndProvider>
</MuiThemeProvider>
Expand Down
2 changes: 2 additions & 0 deletions demo/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,15 @@ export class AppModule {
doubleOptInTemplateId: config.brevo.doubleOptInTemplateId,
sender: { name: config.brevo.sender.name, email: config.brevo.sender.email },
allowedRedirectUrl: config.brevo.allowedRedirectUrl,
redirectUrlForImport: config.brevo.redirectUrlForImport,
};
} else {
return {
apiKey: config.brevo.apiKey,
doubleOptInTemplateId: config.brevo.doubleOptInTemplateId,
sender: { name: config.brevo.sender.name, email: config.brevo.sender.email },
allowedRedirectUrl: config.brevo.allowedRedirectUrl,
redirectUrlForImport: config.brevo.redirectUrlForImport,
};
}
},
Expand Down
1 change: 1 addition & 0 deletions demo/api/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function createConfig(processEnv: NodeJS.ProcessEnv) {
name: envVars.BREVO_SENDER_NAME,
email: envVars.BREVO_SENDER_EMAIL,
},
redirectUrlForImport: envVars.REDIRECT_URL_FOR_IMPORT,
},
campaign: {
url: envVars.CAMPAIGN_URL,
Expand Down
3 changes: 3 additions & 0 deletions demo/api/src/config/environment-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ export class EnvironmentVariables {
@IsEmail()
BREVO_SENDER_EMAIL: string;

@IsString()
REDIRECT_URL_FOR_IMPORT: string;

@IsString()
ECG_RTR_LIST_API_KEY: string;

Expand Down
3 changes: 2 additions & 1 deletion packages/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"dependencies": {
"date-fns": "^2.28.0",
"file-saver": "^2.0.5",
"lodash.isequal": "^4.0.0"
"lodash.isequal": "^4.0.0",
"react-dropzone": "^14.2.3"
},
"devDependencies": {
"@apollo/client": "^3.2.5",
Expand Down
48 changes: 31 additions & 17 deletions packages/admin/src/brevoContacts/BrevoContactsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import { DataGrid, GridColDef, GridToolbarQuickFilter } from "@mui/x-data-grid";
import * as React from "react";
import { FormattedMessage, IntlShape, useIntl } from "react-intl";

import { useContactImportFromCsv } from "../common/contactImport/useContactImportFromCsv";
import { GQLEmailCampaignContentScopeInput } from "../graphql.generated";
import { CrudMoreActionsMenu } from "../temp/CrudMoreActionsMenu";
import {
GQLBrevoContactsGridQuery,
GQLBrevoContactsGridQueryVariables,
Expand All @@ -29,6 +32,7 @@ import {
GQLDeleteBrevoContactMutationVariables,
GQLUpdateBrevoContactMutation,
GQLUpdateBrevoContactMutationVariables,
namedOperations,
} from "./BrevoContactsGrid.generated";

const brevoContactsFragment = gql`
Expand Down Expand Up @@ -56,24 +60,33 @@ const updateBrevoContactMutation = gql`
}
`;

function BrevoContactsGridToolbar({ intl }: { intl: IntlShape }) {
function BrevoContactsGridToolbar({ intl, scope }: { intl: IntlShape; scope: GQLEmailCampaignContentScopeInput }) {
const [moreActionsMenuItem, contactImportComponent] = useContactImportFromCsv({
scope,
refetchQueries: [namedOperations.Query.BrevoContactsGrid],
});

return (
<Toolbar>
<ToolbarTitleItem>
<FormattedMessage id="cometBrevoModule.brevoContact.title" defaultMessage="Contacts" />
</ToolbarTitleItem>
<ToolbarItem>
<GridToolbarQuickFilter
placeholder={intl.formatMessage({ id: "cometBrevoModule.brevoContact.searchEmail", defaultMessage: "Search email address" })}
/>
</ToolbarItem>
<ToolbarFillSpace />
<ToolbarActions>
<Button startIcon={<Add />} component={StackLink} pageName="add" payload="add" variant="contained" color="primary">
<FormattedMessage id="cometBrevoModule.brevoContact.newContact" defaultMessage="New contact" />
</Button>
</ToolbarActions>
</Toolbar>
<>
<Toolbar>
<ToolbarTitleItem>
<FormattedMessage id="cometBrevoModule.brevoContact.title" defaultMessage="Contacts" />
</ToolbarTitleItem>
<ToolbarItem>
<GridToolbarQuickFilter
placeholder={intl.formatMessage({ id: "cometBrevoModule.brevoContact.searchEmail", defaultMessage: "Search email address" })}
/>
</ToolbarItem>
<ToolbarFillSpace />
<ToolbarActions>
<CrudMoreActionsMenu overallItems={[moreActionsMenuItem]} />
<Button startIcon={<Add />} component={StackLink} pageName="add" payload="add" variant="contained" color="primary">
<FormattedMessage id="cometBrevoModule.brevoContact.newContact" defaultMessage="New contact" />
</Button>
</ToolbarActions>
</Toolbar>
{contactImportComponent}
</>
);
}

Expand Down Expand Up @@ -216,6 +229,7 @@ export function BrevoContactsGrid({
componentsProps={{
toolbar: {
intl,
scope,
},
}}
/>
Expand Down
23 changes: 23 additions & 0 deletions packages/admin/src/common/BrevoConfigProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";

export interface BrevoConfig {
apiUrl: string;
}

const BrevoConfigContext = React.createContext<BrevoConfig | undefined>(undefined);

interface BrevoConfigProviderProps {
value: BrevoConfig;
}

export const BrevoConfigProvider = ({ children, value }: React.PropsWithChildren<BrevoConfigProviderProps>) => {
return <BrevoConfigContext.Provider value={value}>{children}</BrevoConfigContext.Provider>;
};

export const useBrevoConfig = (): BrevoConfig => {
const context = React.useContext(BrevoConfigContext);
if (context === undefined) {
throw new Error("useBrevoConfig must be used within a BrevoConfigProvider");
}
return context;
};
Loading

0 comments on commit aae0de4

Please sign in to comment.